TypeScript에서 중요한 것
TypeScript는 JavaScript의 자유로운 타입 정의를 명확하게 하기 위해 나온 언어입니다.
그러다 보니 JavaScript에서 사용하던 Function 방식을 사용하다 보면 자주 타입과 관련된 에러를 마주하곤 합니다. 이와 같은 상황을 피하기 위해 TypeScript에서의 Function을 좀 더 명확하게 이해하기 위해 작성하게 되었습니다.
Function type expression
💡 함수를 정의하는 가장 간단한 방법 중 하나입니다. 화살표 함수와 유사합니다.
function greeter(fn: (a: string) => void) {
fn("I'm Function");
}
function printToConsole(s: string) => {
console.log(s);
}
greeter(printToConsole);
------------------------------------------------------------------------
type GreeterFunc = (a:string) => void;
function greeter(fn: GreetFunc) {
}
위 코드를 보면 parameter에 Type이 선언되어 있습니다. 그리고 function은 type으로도 만들 수 있습니다.
Call Signature
type을 사용하여 함수를 정의하는 방법입니다. 화살표 대신 : 를 사용합니다. Call Signature 방식은 여러 property를 갖는 object에 호출 능력을 부여합니다. fn.description으로 property에 접근이 가능하고, fn(1)로 함수처럼 호출할 수도 있습니다.
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(1));
}
Call Signature 앞에 new 키워드를 사용하여 호출 능력 기능을 추가할 수도 있습니다. 즉, 생성자를 만들 수 있습니다.
type SomeConstructor = {
new (s: string): SomeObject
}
function fn(ctor: SomeConstructor) {
return new ctor("Ok!")
}
위 코드와 같이 함수를 new 연산자로 호출이 가능해집니다.
Generic Function
TypeScript는 JavaScript와 달리 모든 변수에 타입을 지정해줘야 합니다. 그러다보니 어떤 값이든 들어올 수 있는 any 타입을 사용해버리는 경우가 있습니다.
function firstElement(arr: any[]) {
return arr[0]
}
여기서 any를 사용하면 반환 타입도 any로 나가게 됩니다. 왜냐하면 arr는 any 타입의 배열이기 때문입니다.
그렇다면 어떤 방법을 사용해야 입력 값의 타입을 정확하게 반환할까요?
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0]
}
위와 같은 코드를 작성하면 들어오는 파라미터 타입에 따라 반환 값도 타입이 결정됩니다.
그러면 아래와 같이 사용할 때 추론이 가능해집니다.
const s = firstElement(["A", "B", "C", "D"]) <em>// return String</em>
const n = firstElement([1, 2, 3, 4, 5, 6]) <em>// return Number</em>
TypeScript는 타입을 추론할 수 있습니다. 그 과정에서 제약을 걸어 특정한 상황에서는 특정 타입을 제한하는 기능을 제공합니다.
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
const longerArray = longest([1, 2], [1, 2, 3, 4]);
const longerString = longest("aassdd", "hello");
const error = longest(10, 20); <em>// error가 발생한다.</em>
length 속성을 Number로 제한했기 때문에 length가 number를 가지지 못하는 error 변수는 에러가 발생합니다. longerArray, longerString은 length를 number로 추론이 가능했기 때문에 문제없이 동작하게 됩니다.
물론 무조건 추론할 수 있는 건 아닙니다.
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2)
const arr = combine([1, 2, 3], ["hello"]); // error 발생
//Type 'string' is not assignable to type 'number'
위와 같은 상황이 발생할 수 있습니다. TypeScript에서 추론은 해주지만 모든 상황에서 추론할 수 있는 것은 아닙니다. 위에 상황은 해결책이 있습니다.
const arr = combine<string | number>([2, 3, 4], ["Hi, Hello"]);
TypeScript가 타입을 알 수 있도록 string과 number로 타입을 정의 했습니다. 이와 같이 사용하면 확장성 있게 함수를 사용할 수 있습니다.
좋은 제네릭 함수 작성 방법
function firstElement1<Type>(arr: Type[]) {
return arr[0]
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0]
}
두 함수 중에서 어떤 함수가 좋은 방식일까요?
firstElement1 함수가 좋은 방식입니다. firstElement1은 반환 값이 Type입니다. firstElement2 함수는 반환 값이 any입니다. 즉, 유추된 반환 값이 입력 타입을 따라가기 때문에 추론하기 좋습니다. 가능하면 형식 매개변수 자체에 타입을 제한하지 말고 사용해야 합니다. firstElement2는 한번 더 타입 추론 과정을 거쳐야 합니다.
또한 function을 만들 때, 유형 매개변수(type parameters)는 적게 사용해야 합니다.
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func)
}
function filter2<Type, Func extends (arg: Type) => boolean> (
arr: Type[],
func: Func
): Type[] {
return arr.filter(func)
}
filter1은 parameter에 추론 가능한 유형을 선언했습니다. filter2는 type parameter에 추론 가능한 유형을 선언했습니다. 이렇게 사용했을 때 filter1이 더 이해하기 쉽고 사용하기 좋습니다.
parameter에 정확한 타입이 있어야 개발자 입장에서 어떻게 사용하는 지 알 수 있습니다. type parameter는 최대한 적게 사용하는 것이 좋습니다.
마지막으로 type parameters는 두 번 나타나야 합니다. 우리가 제네릭를 사용하는 이유를 생각해야 합니다.
제네릭을 사용하는 가장 큰 이유는 type이 두 번 이상 다르게 사용되기 때문입니다.
function greet<Str extends string>(s: Str) {
console.log("Hi, " + s)
}
greet("world")
위 코드처럼 사용하는 함수는 제네릭을 사용할 의미가 없습니다. 항상 string 값만 활용하기 때문입니다.
function greet(s: string) {
console.log("Hello, " + s)
}
우리가 왜 제네릭을 사용하는지 꼭 생각하시길 바랍니다. type parameters는 여러 값의 Type을 연결하기 위해 사용합니다.
선택적 매개변수(Optional Parameters)
우리는 parameter에 optional 형식을 정의하여 인수를 가변적으로 사용할 수 있습니다.
function f(a?: string) {
console.log("I like " + a)
}
f() // I like
f("money") // I like money
위와 같이 사용할 수 있으며, 기본값을 정의할 수도 있습니다.
function f(x = 1) {
console.log("x = " + x)
}
f() // x = 1
f(10) // x = 10
선택적 매개변수 및 기본값을 활용하여 유연한 함수 작성이 가능합니다. optional 형식의 파라미터는 아무런 값이 없을 때는 undefined 형식을 가집니다. 파라미터 체크 시 유의해야 합니다.
결론
JavaScript에서 TypeScript로 마이그레이션을 진행할 때 도움이 많이 된 내용들입니다.
아래 레퍼런스에서 더 자세한 내용과 다양한 정보를 얻을 수 있습니다.
레퍼런스
https://www.typescriptlang.org/docs/handbook/2/functions.html#function