infer는 조건부 타입 안에서 타입을 캡처해서 변수처럼 사용 할 수 있게 해 주는 키워드입니다.

기본 문법

TYPESCRIPT
// T가 배열이면 그 요소의 타입을 추출
type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;    // string
type B = ElementOf<number[]>;    // number
type C = ElementOf<string>;      // never (배열이 아니므로)

infer E는 "여기에 뭔가 타입이 있을 텐데, 그걸 E로 캡처하겠다"는 의미입니다.

ReturnType의 비밀

ReturnType이 어떻게 함수의 반환 타입을 추출하는지 infer로 이해할 수 있습니다.

TYPESCRIPT
// ReturnType 내부 구현
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;
//                            ^^^^^^^ 반환 타입을 R로 캡처

type A = ReturnType<() => string>;         // string
type B = ReturnType<() => Promise<number>>; // Promise<number>
TYPESCRIPT
// Parameters 내부 구현
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;
//                    ^^^^^^^ 매개변수 타입을 P로 캡처

type C = Parameters<(a: string, b: number) => void>;
// [a: string, b: number]

infer 활용 패턴

Promise 내부 타입 추출

TYPESCRIPT
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<Promise<number>>;  // number
type C = UnwrapPromise<string>;            // string (Promise가 아니면 그대로)

// 중첩 Promise도 재귀적으로 풀기
type DeepUnwrapPromise<T> = T extends Promise<infer U>
  ? DeepUnwrapPromise<U>
  : T;

type D = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>;
// string

면접에서 "Awaited 타입을 직접 구현해 보세요"라고 하면 이 패턴을 사용합니다. TypeScript의 내장 Awaited<T>도 이런 원리입니다.

배열 첫 번째/마지막 요소 타입

TYPESCRIPT
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type A = First<[string, number, boolean]>; // string
type B = Last<[string, number, boolean]>;  // boolean
type C = First<[]>;                         // never

문자열 파싱

TYPESCRIPT
// 문자열의 첫 글자 추출
type FirstChar<S extends string> = S extends `${infer C}${string}` ? C : never;

type A = FirstChar<'hello'>; // 'h'
type B = FirstChar<''>;      // never

// 문자열 트리밍
type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? TrimLeft<Rest> : S;

type C = TrimLeft<'   hello'>; // 'hello'

함수 첫 번째 매개변수 추출

TYPESCRIPT
type FirstArg<T extends (...args: any) => any> =
  T extends (first: infer A, ...rest: any[]) => any ? A : never;

type A = FirstArg<(name: string, age: number) => void>; // string
type B = FirstArg<() => void>;                           // unknown (매개변수 없음)

infer의 위치에 따른 동작

TYPESCRIPT
// 같은 위치에 여러 infer — 유니온 또는 인터섹션으로 결합

// 공변 위치 (반환 타입 등) — 유니온
type ReturnUnion<T> = T extends {
  a: () => infer R;
  b: () => infer R;
} ? R : never;

type Test1 = ReturnUnion<{ a: () => string; b: () => number }>;
// string | number (유니온)

// 반공변 위치 (매개변수 등) — 인터섹션
type ParamIntersection<T> = T extends {
  a: (x: infer P) => void;
  b: (x: infer P) => void;
} ? P : never;

type Test2 = ParamIntersection<{ a: (x: { name: string }) => void; b: (x: { age: number }) => void }>;
// { name: string } & { age: number } (인터섹션)

공부하다 보니 이 공변/반공변 동작이 고급 타입 프로그래밍에서 핵심이더라고요.

TS 5.0: const 타입 매개변수에서의 infer

TYPESCRIPT
// infer에도 extends 제약을 걸 수 있음 (TS 4.7+)
type FirstString<T> = T extends [infer S extends string, ...any[]] ? S : never;

type A = FirstString<['hello', 42]>;    // 'hello'
type B = FirstString<[42, 'hello']>;    // never (첫 번째가 string이 아님)

정리

  • infer는 조건부 타입 안에서 타입을 캡처하는 키워드다
  • ReturnType, Parameters 등 내장 유틸리티가 infer로 구현되어 있다
  • Promise 언래핑, 배열 요소 추출, 문자열 파싱 등에 활용한다
  • 공변 위치의 infer는 유니온, 반공변 위치의 infer는 인터섹션으로 결합된다
  • TS 4.7+에서는 infer T extends U로 캡처와 제약을 동시에 할 수 있다
댓글 로딩 중...