typescript infer 언제 사용할까?

@JunYong · December 10, 2022 · 5 min read

TypeScript Infer
infer 알고 쓰자
infer 에러 발생 case
infer를 사용하여 객체 key 타입 추론하기
결론
참조

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] 됩니다.

참조

@JunYong
Hello :) I'm Jun.D FrontEnd Developer