https://www.typescriptlang.org/ko/docs/handbook/2/functions.html
함수 타입 표현식
함수를 설명하는 가장 간단한 방법은 함수 타입 표현식이다.
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
(a: string) => void는 문자열 타입 a를 하나의 매개변수로 가지고 반환값이 없는 함수를 의미한다. 매개변수의 타입이 지정되지 않으면 암묵적으로 any가 된다.
호출 시그니처(Call Signatures)
만약 호출 가능하면서 프로퍼티를 가진 무언가를 설명하려고 하면, 객체 타입에 호출 시그니처를 사용해 표현할 수 있다.
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
이 문법은 함수 타입 표현식과 다르다. 매개변수 타입과 반환값의 타입 사이에 =>가 아닌 :을 사용해야 한다.
구성 시그니처(Construct Signatures)
new 연산자는 주로 새로운 객체를 생성하는 데 사용되기 때문에 타입스크립트는 new를 붙인 것을 생성자로 간주한다.
호출 시그니처 앞에 new 키워드를 붙임으로서 구성 시그니처를 작성할 수 있다.
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
제네릭 함수
입력 값이 출력 값의 타입과 관련이 있거나, 두 입력값의 타입이 서로 관련이 있는 형태의 함수를 작성하는 것은 흔히 일어나는 일이다.
배열의 첫 번째 원소를 반환하는 함수가 있다고 해보자.
function firstElement(arr: any[]) {
return arr[0];
}
함수는 제 역할을 하지만 아쉽게도 반환 타입이 any이다. 함수가 배열 원소의 타입도 반환한다면 더 좋을 것이다.
타입스크립트의 제네릭 문법은 두 값 사이의 상관관계를 표현하기 위해 사용된다. 타입 매개변수를 선언함으로써 그런 표현을 할 수 있다.
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
타입 매개변수 Type을 함수에 선언하고, 필요한 두 곳에 사용함으로써 함수의 입력값과 출력값 가시에 연결고리를 만들었다. 이렇게 하면 이 함수를 호출할 때 더 명확한 타입을 얻을 수 있다.
// s는 "string" 타입
const s = firstElement(["a", "b", "c"]);
// n은 "number" 타입
const n = firstElement([1, 2, 3]);
// u는 "undefined" 타입
const u = firstElement([]);
추론(Inference)
위 예제에서 따로 타입을 특정하지 않았음에 주목하자. 여기서 타입은 타입스크립트에 의해 자동적으로 추론된 것이다.
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// 매개변수 'n'의 타입은 'string' 입니다.
// 'parsed'는 number[] 타입을 하고 있습니다.
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
이 예제에서 Input 타입은 입력으로 주어진 string 타입의 배열로부터 타입을 추론할 수 있고, Output의 타입은 함수 표현식의 반환 값(number)을 통해 추론할 수 있다.
타입 제한 조건
위의 예시들에서는 모든 타입에 대해 동작하는 제네릭 함수들을 작성했다.
하지만 가끔은 특정한 값들의 부분집합에 대해서만 동작하기를 원할 때가 있다. 이런 경우 타입 제한 조건을 사용해 타입 매개변수가 받아들일 수 있는 타입들을 제한할 수 있다.
두 값중에 더 긴 것을 반환하는 함수를 작성해보자. 이 작업을 위해 number 타입인 length 프로퍼티가 필요하다. extends를 사용해 타입 매개변수를 그 타입으로 제한할 수 있다.
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray 의 타입은 'number[]' 입니다'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 의 타입은 'alice' | 'bob' 입니다.
const longerString = longest("alice", "bob");
// 에러! Number에는 'length' 프로퍼티가 없습니다.
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
const notOK = longest(10, 100);
Type을 { length: number }로 제한했기 때문에 a, b 매개변수에 대해 .length 프로퍼티에 접근할 수 있다. 타입 제한이 없다면 length 프로퍼티를 가지지 않는 타입이 들어올 수도 있었을 것이다.
(longerArray, longerString의 타입은 인수를 기반으로 추론되었다.)
타입 인수를 명시하기
타입스크립트는 제네릭 호출에서 의도된 타입을 대체로 추론해내지만 항상 그렇지는 않다.
두 배열을 결합하는 함수를 작성했다고 해보자.
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
만약 타입이 다른 배열과 함께 해당 함수를 사용하는 것은 잘못된 것일 것이다.
const arr = combine([1, 2, 3], ["hello"]);
// Type 'string' is not assignable to type 'number'.
만약 이런 것을 의도했다면 수동으로 Type을 명시해주어야 한다.
const arr = combine<string | number>([1, 2, 3], ["hello"]);
좋은 제네릭 함수를 작성하기 위한 가이드라인
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
firstElement1과 firstElement2는 동일해보일 수 있지만 firstElement1이 더 좋은 방법이다.
firstElement의 추론된 반환 타입은 Type이다.
타입스크립트는 호출 중에 타입을 해석하기 위해 기다리기보다 호출 시점에 타입 제한 조건을 이용해 해석하기 때문에 firstElement2의 arr[0]의 타입은 any가 된다.
따라서 가능하다면 타입 매개변수를 제약하기보다 타입 매개변수 그 자체를 사용하자.
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);
}
filter2는 두 값을 연관시키지 않는 타입 매개변수 Func를 만들었다. Func는 함수를 더 읽고 이해하기 어렵게만 만들 뿐 아무것도 하지 않는다.
가능하다면 항상 타입 매개변수는 최소로 사용하자.
가끔은 함수에서 제네릭이 필요없을 수도 있다는 사실을 간과한다.
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
위 함수는 아래와 같이 간단한 버전을 쉽게 작성할 수 있을 것이다.
function greet(s: string) {
console.log("Hello, " + s);
}
타입 매개변수는 여러 값의 타입을 연관시키는 용도로 사용함을 기억하자. 만약 타입 매개변수가 함수 시그니처에서 한 번만 사용되었다면 어떤 것도 연관시키지 않고 있는 것이다.
만약 타입 매개변수가 한 곳에서만 나온다면 정말로 필요한 것인지 다시 생각해보자.
'TIL' 카테고리의 다른 글
[TIL] 23.01.17 프리온보딩 챌린지 3회차 (0) | 2023.01.18 |
---|---|
[TIL] 23.01.15 타입스크립트 핸드북 - More on Functions(2) (0) | 2023.01.16 |
[TIL] 23.01.10 프리온보딩 챌린지를 시작하며 (0) | 2023.01.11 |
[TIL] 22.12.30 타입스크립트 핸드북 - Everyday Types (0) | 2022.12.31 |
[TIL] 22.12.28 JWT 토큰 (0) | 2022.12.28 |