TypeScript 제네릭 타입 정리
타입에 변수를 주는 것, 그러니까 타입을 파라미터처럼 다루는 것이 제네릭이다. 어떤 타입이 들어올지 호출 시점에 결정하고 싶을 때 쓴다.
선언과 호출은 어떻게 생겼나
함수명과 매개변수 사이에 <타입변수>를 적는 게 시작이다.
function getText<T>(text: T): T {
return text;
}호출할 때는 타입을 명시할 수도 있고, 인자에서 추론하게 둘 수도 있다.
getText<string>('hi'); // T = string
getText<number>(10); // T = number
getText<boolean>(true); // T = boolean
getText('hi'); // T = string (추론)
getText(10); // T = number (추론)
getText(true); // T = boolean (추론)배열 인자에 제네릭을 줄 때
인자가 배열일 때도 제네릭을 그대로 쓸 수 있다. T[]와 Array<T>는 같은 표현이다.
function getText<T>(text: T[]): T[] {
return text;
}
function getText<T>(text: Array<T>): Array<T> {
return text;
}제네릭 함수 타입을 변수에 담는 두 가지 표기
같은 제네릭 함수를 변수 타입으로 적는 방법이 두 가지 있다. 둘은 등가다.
function logText<T>(text: T): T {
return text;
}
let str1: <T>(text: T) => T = logText;
let str3: {<T>(text: T): T} = logText;차이는 반환 타입을 적는 기호다. 함수 타입 표기에서는 =>로 적고, 객체/인터페이스 안의 call signature에서는 :로 적는다.
인터페이스로 분리하면 두 형태가 갈린다
logText의 타입을 인터페이스로 빼낼 때, 비슷해 보이는 두 형태가 있다. 그런데 둘은 서로 다른 타입을 정의한다.
방식 A: 제네릭 호출 시그니처를 가진 인터페이스
interface GenericType {
<T>(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericType = logText;인터페이스 자체는 제네릭이 아니다. T는 호출 시점에 결정된다. 그래서 같은 myString 변수에 myString(1)도, myString('hi')도 모두 호출할 수 있다.
방식 B: 제네릭 인터페이스
interface GenericType<T> {
(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericType<string> = logText;인터페이스 자체가 제네릭이라 변수를 선언할 때 GenericType<string>처럼 T를 고정해야 한다. 그렇게 잠긴 myString은 string만 받는다.
호출 시점에 T가 정해지느냐(A), 변수 선언 시점에 잠기느냐(B). 형태는 비슷하지만 잠기는 타이밍이 다르기 때문에 같은 타입이라고 묶을 수 없다.
객체 키를 인자로 받을 때는 keyof
객체의 키를 인자로 받는 함수를 만들 때, 임의 문자열이 들어오는 걸 막고 그 객체가 실제로 가진 키만 허용하고 싶었다. 이럴 때 keyof를 쓴다.
function getProperty<T, O extends keyof T>(obj: T, key: O) {
return obj[key];
}
let obj = { a: 1, b: 2, c: 3 };
getProperty(obj, "a"); // okay
getProperty(obj, "z"); // error: "z"는 obj의 프로퍼티 키가 아님O extends keyof T는 "O는 T의 키 중 하나여야 한다"는 제약이다. obj의 키가 "a" | "b" | "c"로 한정되니까, "z"를 넘기면 컴파일 단계에서 잡힌다.