TypeScript Infer
유틸로 제공되는 타입(pick,Parameters,Extract,Omit,Exclude,Record, ...etc) 타입을 보는 중에 Parameters 타입을 보다보니 infer라는 문법이 눈에 띄었습니다.
type Parameters<T extends (...args: any) => any> = T extends
(...args: infer P) => any ? P : never;
어떤식으로 작동을 하는지 궁금했습니다.
infer
키워드는 TypeScript 의 타입 추론(type inference) 기능에 사용됩니다.
이 기능은 타입 추론 연산자를 사용해서 타입을 추론하는 것을 돕기 위해 사용되며,
infer
는 조건부 타입에서 true로 평가 될때 사용이 되며, 타입을 새로 저장하여 타입을 추출 합니다.
즉 'infer' 키워드는 조건부 타입(extends) 조건부타입
에서 infer를 사용합니다.
T extends infer U ? U : Y
또 'infer'는 전달된 인자의 타입을 추론하기 위한 제네릭 타입입니다.
function getType<T>(x: T): infer U {
return x;
}
const result = getType("hello");
// result의 타입은 string입니다.
infer 키워드를 사용하면 제네릭 함수의 인자의 타입을 추론할 수 있습니다. 위 예제에서 getType 함수는 T라는 제네릭 타입을 인자로 전달받고 infer U를 사용해 T의 타입을 추론한 결과를 반환합니다. 결과적으로 result의 타입은 string입니다.
즉 infer 키워드는 제네릭 타입 추론에서 유용한 기능을 제공합니다. 제네릭 타입을 정의할 때 infer 키워드를 사용하면, 제네릭 타입을 직접 지정하지 않고도 제네릭 함수의 인자의 타입을 추론할 수 있습니다.
infer 알고 쓰자
간단한 예제1
type Sample<T> = T extends infer R ? R : undefined;
const a1 :Sample<number> = 1;
// a1의 타입은 number;
const a2: Sample<()=>void> = () => console.log(1);
// a2의 타입은 ()=>void
T extends infer R ? R : undefined;
T 가 infer R 이면 R 이고 아니면 undefined 타입을 반환 됩니다.
Sample<number>
T가 number 이고 infer가 추론한 타입 R도 number 이다. true 조건이 성립되어
추론한 R 타입도 number가 반환됩니다.
Sample<()=>void>
T가 ()=>void 타입이고 infer가 추론한 타입 R도 ()=>void 이다. true 조건이 성립되어
추론한 R 타입도 ()=>void가 반환됩니다.
간단한 예제2
type Sample2<T> = T extends (infer R)[] ? R : undefined;
const a3: Sample2<number> = undefined;
const a4: Sample2<number[]> = 1;
Sample2<number>
T가 inter 타입에 배열이면 추론한 타입 R이 되고 아니면 undefined가 반환 합니다.
즉. T는 number이고 (inter R)[]이 아니라서 undefined가 반환 됩니다.
Sample2<number[]>
T가 number[] 이고, infer R은 number가 추론 되며, 즉 number[]가 되어 true
조건이 성립되어 infer에서 추론한 R타입인 number가 반환 됩니다.
간단한 예제3
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
function getPropertyValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key] as infer U;
}
const person = { name: 'John', age: 30 };
const name1 = getProperty(person, 'name'); // string | number
const name2 = getPropertyValue(person, 'name'); // string
getPropertyValue함수에서 반환 유형의 구문을 사용 하면 함수 infer U에서 반환하는 값의 유형을 추출할 수 있습니다. 이를 통해 key매개변수 유형을 사용하는 것보다 함수에 대해 더 정확한 반환 유형을 지정할 수 있습니다.
infer 에러 발생 case
infer 타입은 위에서조건부 타입에서 true로 평가 될때 사용이 되며, 타입을 새로 저장하여 타입을 추출 한다고 했습니다. 만약 다른 방식으로 사용되는 케이스는 에러가 발생합니다.
type inferError<T extends (infer U)[]> = T[0]
// 조건부 타입에서 true로 평가 될때 사용하지 않아서 에러가 발생 합니다.
type inferError<T> = (infer U)[] extends T ? U : T
// 조건부 타입에서 사용하지 않아 에러가 발생 합니다.
type inferError<T> = T extends (infer U)[] ? T : U
// 조건부 타입에서 사용했지만, false로 평가 될때 U를 추론하였기 때문에 에러가 발생 합니다.
infer를 사용하여 객체 key 타입 추론하기
조건부 타입과 infer를 사용하여 객체 key 타입을 추론해서 가져올 수 있습니다.
type Car = {
carNumber: number;
carName: string;
}
type carArrayTyep<T> = T extends { carNumber: infer U, carName: infer R } ? [U, R] : T
type C3 = carArrayTyep<Car> // [number, string]
carNumber에 infer U 타입을 추론하고, carName에 infer R 타입을 추론해서 true 평가 되면 추론한 타입 [U, R]을 반환
하게 됩니다.
C3
타입은 [number, string]
가 됩니다.
type Car = {
carNumber: number;
carName: string;
}
type carArrayTyep<T> = T extends { carNumber: infer U, carName: infer U} ? U : T
type C3 = carArrayTyep<Car>
서로 다른 객체 키에 대한 타입을 하나의 infer U 타입으로 추론을 하게 되면 string | number
union 타입을
반환하게 됩니다. 이유는 공변(covariant)
같은 위치에 여러개 타입이 존재하는 경우 최종 타입이 union으로 타입이
추론 됩니다.
타입스크립트에서는 공변성을 갖고 있지만, 함수의 매개변수는 반공변을 갖고 있습니다
type Food<T> = T extends { name: (x: infer U) => void, cook: (x: infer U) => void } ? U : never;
type F = Food<{ name: (x: string) => void, cook: (x: number) => void }>;
이번에는 반공변(contravariant)
에 대하여 확인 해보겠습니다.
반공변은 같은 위치에 여러개 타입이 존재하는 경우 교차(intersection) 타입이 추론 됩니다
즉 위에 예시 F
에 대한 타입은 string & number
타입이 추론 됩니다.
공변(covariant)
, 반공변(contravariant)
에 대하여 알아봅시다.
고차 타입 X은 Array고차 타입
이라고 한다.
공변
은 임의 고차타입 X에 대하여 A => B일 때 X< A > => X< B > 이면 X는 공변 타입이다.
반공변
은 임의 고차타입 X에 대하여 A => B일 떄 X< B > => X< A > 이면 X는 반공변 타입이다.
-
타입을 번환하는 함수 타입은 공변한다.
type C<T> = (arr: string[]) => T
-
함수가 아닌 타입은 공변한다.
type C1<T> = T & { id: number }
-
제네릭 타입을 매개변수로 사용하는 함수타입은 반공변 한다.
type C2<T> = (v: T) => number
type Food<T> = T extends { name: (x: infer U) => void, cook: (x: infer U) => void } ? U : never;
type F = Food<{ name: (x: string) => void, cook: (x: number) => void }>;
위에서 작상한 에시는 반공변
하기 떄문에 `string & number' 타입이 추론 되었지만
공변하도록 변경을 하게 되면 아래와 같습니다.
type Food<T> = T extends { name: (x: string) => infer U, cook: (x:number) => infer U } ? U : never;
type F = Food<{ name: (x: string) => string, cook: (x: number) => number }>;
함수의 반환 타입을 infer U로 추론하고 있어 공변
합니다. 즉 F타입은 string | number
가 반환 됩니다.
공변, 반공변에 대한 좀 더 자세한 글은 여기 링크 에서 확인 해주세요
결론
type Parameters<T extends (...args: any) => any> = T extends
(...args: infer P) => any ? P : never;
Parameters 타입에서 시작 되었으니, 타입에 대하여 풀어보면
T
가 조건부 타입 (...args: infer P) => any
이면 true가 성립이 되면 ...args
타입 infer P를 추론하여
P
타입을 반환하고 아니면 never
를 반환 합니다.
const abSum = (a:number,b:number) => {
return a + b;
}
type paramSumTS = Parameters<typeof abSum>;
...args
는(arguments Array 객체이며) a, b는 [a,b]가 되고 a
타입은 number , b
타입도 number
즉 infer P
에 P 타입을 추론하면 [a: number, b: number]
타입이 반환 됩니다. paramSumTS 타입은 [a: number, b: number]
됩니다.