infer 키워드 — 조건부 타입에서 타입 추출하기
infer는 조건부 타입 안에서 타입을 캡처해서 변수처럼 사용 할 수 있게 해 주는 키워드입니다.
기본 문법
// 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로 이해할 수 있습니다.
// 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>
// 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 내부 타입 추출
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>도 이런 원리입니다.
배열 첫 번째/마지막 요소 타입
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
문자열 파싱
// 문자열의 첫 글자 추출
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'
함수 첫 번째 매개변수 추출
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의 위치에 따른 동작
// 같은 위치에 여러 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
// 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로 캡처와 제약을 동시에 할 수 있다
댓글 로딩 중...