TypeScript infer 키워드의 고급 활용 패턴, infer extends 구문, 공변/반변 위치에서의 추론, 그리고 실전 타입 추출 패턴을 심층 분석합니다.
7장에서 템플릿 리터럴 타입의 패턴 매칭을 다뤘습니다. 이 장에서는 그 핵심 도구인 infer 키워드를 심층적으로 분석합니다. infer는 조건부 타입 내에서 타입을 추출하는 메커니즘으로, TypeScript 타입 시스템에서 가장 강력한 도구 중 하나입니다. 특히 TypeScript 4.7에서 도입된 infer extends 구문과 5.x에서의 추론 개선 사항을 집중적으로 다룹니다.
infer는 조건부 타입의 extends 절에서 타입 변수를 선언하는 키워드입니다. 패턴 매칭에서 캡처 그룹과 유사하게, 타입 구조의 특정 부분을 "캡처"합니다.
// 패턴: (...args) => R 에서 R을 캡처
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 패턴: Promise<T>에서 T를 캡처
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// 패턴: [first, ...rest]에서 first와 rest를 캡처
type First<T> = T extends [infer F, ...any[]] ? F : never;
type Rest<T> = T extends [any, ...infer R] ? R : never;
type F = First<[1, 2, 3]>; // 1
type R = Rest<[1, 2, 3]>; // [2, 3]같은 타입 변수를 여러 위치에서 추론하면, TypeScript는 모든 후보를 수집하여 결과를 결정합니다.
// 공변(covariant) 위치: 유니온으로 결합
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type F1 = Foo<{ a: string; b: number }>; // string | number
// 반변(contravariant) 위치: 인터섹션으로 결합
type Bar<T> = T extends {
a: (x: infer U) => void;
b: (x: infer U) => void;
} ? U : never;
type B1 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
// string & number → never공변(Covariant) 위치란 타입이 "출력"되는 위치(반환 타입, 프로퍼티 값 등)이고, 반변(Contravariant) 위치란 타입이 "입력"되는 위치(매개변수 등)입니다. infer가 공변 위치에 있으면 유니온, 반변 위치에 있으면 인터섹션으로 결합됩니다.
이 특성을 활용하면 유니온을 인터섹션으로 변환하는 유명한 타입을 구현할 수 있습니다.
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends (x: infer I) => void
? I
: never;
type Result = UnionToIntersection<{ a: 1 } | { b: 2 } | { c: 3 }>;
// { a: 1 } & { b: 2 } & { c: 3 }TypeScript 4.7에서 도입된 infer extends 구문은 추론되는 타입에 제약 조건을 추가합니다. 이를 통해 추가적인 조건부 타입 없이 한 번에 타입을 필터링하고 추출할 �� 있습니다.
// 4.7 이전: 두 단계 필요
type FirstStringOld<T> =
T extends [infer F, ...any[]]
? F extends string ? F : never
: never;
// 4.7 이후: 한 단계로 가능
type FirstString<T> =
T extends [infer F extends string, ...any[]] ? F : never;
type FS1 = FirstString<["hello", 1, 2]>; // "hello"
type FS2 = FirstString<[42, "hi"]>; // neverinfer extends number를 사용하면 문자열에서 숫자를 추출할 수 있습니다.
type ParseInt<S extends string> =
S extends `${infer N extends number}` ? N : never;
type N1 = ParseInt<"42">; // 42
type N2 = ParseInt<"100">; // 100
type N3 = ParseInt<"hello">; // never
// 버전 문자열 파싱
type ParseVersion<V extends string> =
V extends `${infer Major extends number}.${infer Minor extends number}.${infer Patch extends number}`
? { major: Major; minor: Minor; patch: Patch }
: never;
type Version = ParseVersion<"5.4.2">;
// { major: 5; minor: 4; patch: 2 }type ParseBoolean<S extends string> =
S extends `${infer B extends boolean}` ? B : never;
type B1 = ParseBoolean<"true">; // true
type B2 = ParseBoolean<"false">; // false
type B3 = ParseBoolean<"yes">; // never// 마지막 요소 추출
type Last<T extends any[]> =
T extends [...any[], infer L] ? L : never;
type L1 = Last<[1, 2, 3]>; // 3
type L2 = Last<["a", "b"]>; // "b"
// 마지막 요소 제거
type DropLast<T extends any[]> =
T extends [...infer Rest, any] ? Rest : never;
type DL = DropLast<[1, 2, 3]>; // [1, 2]
// 튜플 뒤집기
type Reverse<T extends any[]> =
T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type Rev = Reverse<[1, 2, 3, 4]>; // [4, 3, 2, 1]
// 튜플 평탄화
type FlattenTuple<T extends any[]> =
T extends [infer First, ...infer Rest]
? First extends any[]
? [...FlattenTuple<First>, ...FlattenTuple<Rest>]
: [First, ...FlattenTuple<Rest>]
: [];
type Flat = FlattenTuple<[1, [2, 3], [4, [5, 6]]]>;
// [1, 2, 3, 4, 5, 6]// 첫 번째 매개변수 추출
type FirstParameter<T> =
T extends (first: infer P, ...rest: any[]) => any ? P : never;
// 마지막 매개변수 추출
type LastParameter<T> =
T extends (...args: [...any[], infer L]) => any ? L : never;
// 함수의 this 타입 추출
type ThisType<T> =
T extends (this: infer U, ...args: any[]) => any ? U : never;
// 오버로드된 함수의 모든 반환 타입 추출
type OverloadReturnTypes<T> =
T extends {
(...args: any[]): infer R1;
(...args: any[]): infer R2;
} ? R1 | R2 : never;// 클래스의 인스턴스 타입 추출
type InstanceType<T> =
T extends new (...args: any[]) => infer R ? R : never;
// 생성자 매개변수 추출
type ConstructorParameters<T> =
T extends new (...args: infer P) => any ? P : never;
class UserModel {
constructor(
public name: string,
public email: string,
public age: number
) {}
}
type UserInstance = InstanceType<typeof UserModel>; // UserModel
type UserCtorParams = ConstructorParameters<typeof UserModel>;
// [string, string, number]type PipeReturn<Fns extends ((...args: any[]) => any)[], Input> =
Fns extends [infer First extends (...args: any[]) => any, ...infer Rest extends ((...args: any[]) => any)[]]
? PipeReturn<Rest, ReturnType<First>>
: Input;
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B;
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C;
function pipe<A, B, C, D>(
fn1: (a: A) => B,
fn2: (b: B) => C,
fn3: (c: C) => D
): (a: A) => D;
function pipe(...fns: ((...args: any[]) => any)[]) {
return (input: any) => fns.reduce((acc, fn) => fn(acc), input);
}
const transform = pipe(
(x: string) => x.length, // string → number
(n: number) => n > 5, // number → boolean
(b: boolean) => b ? "yes" : "no" // boolean → string
);
const result = transform("TypeScript"); // "yes" (타입: string)type ParseJsonPrimitive<T extends string> =
T extends `${infer N extends number}` ? N :
T extends `${infer B extends boolean}` ? B :
T extends "null" ? null :
T extends `"${infer S}"` ? S :
never;
type JP1 = ParseJsonPrimitive<"42">; // 42
type JP2 = ParseJsonPrimitive<"true">; // true
type JP3 = ParseJsonPrimitive<"null">; // null
type JP4 = ParseJsonPrimitive<'"hello"'>; // "hello"콜백 기반 함수를 Promise 기반으로 변환하는 타입입니다.
type CallbackFn = (...args: [...any[], (error: any, result: any) => void]) => void;
type Promisify<T extends CallbackFn> =
T extends (...args: [...infer Args, (error: any, result: infer R) => void]) => void
? (...args: Args) => Promise<R>
: never;
type ReadFileCb = (
path: string,
encoding: string,
callback: (error: Error | null, data: string) => void
) => void;
type ReadFileAsync = Promisify<ReadFileCb>;
// (path: string, encoding: string) => Promise<string>type DeepAwaited<T> =
T extends Promise<infer U>
? DeepAwaited<U>
: T extends (...args: infer A) => infer R
? (...args: A) => DeepAwaited<R>
: T extends object
? { [K in keyof T]: DeepAwaited<T[K]> }
: T;
type Nested = Promise<Promise<Promise<string>>>;
type Resolved = DeepAwaited<Nested>; // string
type FnWithPromise = () => Promise<{ data: Promise<number[]> }>;
type ResolvedFn = DeepAwaited<FnWithPromise>;
// () => { data: number[] }TypeScript 5.4에서 클로저 내 타입 좁히기(narrowing)가 보존되도록 개선되었습니다. 이는 infer 자체와 직접 관련되지는 않지만, 타입 추론의 정확성을 크게 향상시킵니다.
function process(value: string | number) {
if (typeof value === "string") {
// 5.4 이전: 클로저 안에서 value가 string | number로 확장될 수 있었음
// 5.4 이후: string으로 보존
const fn = () => value.toUpperCase(); // OK in 5.4+
}
}TypeScript 5.5에서는 함수가 타입 가드 역할을 하는지 자동으로 추론합니다.
// 5.5 이전: 수동으로 타입 가드를 명시해야 했음
function isNonNull<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
// 5.5 ���후: 자동 추론
function isNonNull2<T>(value: T) {
return value !== null && value !== undefined;
}
// 반환 타입이 자동으로 value is NonNullable<T>로 추론됨
const values: (string | null)[] = ["hello", null, "world", null];
const filtered = values.filter(isNonNull2);
// 5.5+: string[] (이전에는 (string | null)[]이었음)// 1. infer는 반드시 조건부 타입의 extends 절에서만 사용 가능
type Bad = infer T; // Error
// 2. infer 변수는 true 분기에서만 사용 가능
type AlsoBad<T> = T extends (infer U)[] ? string : U; // Error: U를 false 분기에서 사용
// 3. 과도한 재귀는 성능 문제를 유발
// TypeScript는 재귀 깊이를 제한하며, 초과 시 에러 발생infer를 활용한 복잡한 타입은 가독성이 떨어질 ��� 있습니다. 중간 단계의 타입에 의미 있는 이름을 부여하고, JSDoc 주석으로 의도를 문서화하면 유지보수성이 크�� 향상됩니다.
infer 키워드는 TypeScript 타입 시스템에서 패턴 매칭과 타입 추출의 핵심입니다. 함수 시그니처, 튜플, 문자열, Promise 등 다양한 타입 구조에서 원하는 부분을 정밀하게 추출할 수 있습니다. infer extends(4.7+)는 추출과 필터링을 한 번에 수행하게 해주며, TypeScript 5.x의 추론 개선(클로저 보존, 추론된 타입 가드)은 infer와 함께 전체적인 타입 추론 경험을 향상시킵니다.
다음 장에서는 TypeScript 5.4에서 도입된 NoInfer와 새로운 유틸리티 타입을 다룹니다. 라이브러리 설계에서 타입 추론의 방향을 정밀하게 제어하는 방법을 배웁니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
TypeScript 5.4의 NoInfer 유틸리티 타입과 5.x에서 추가된 새로운 타입 도구들을 활용한 라이브러리 설계 패턴을 다룹니다.
TypeScript 템플릿 리터럴 타입의 원리, 내장 문자열 조작 유틸리티, 패턴 매칭, 그리고 매핑 타입과의 결합을 통한 실전 패턴을 다룹니다.
TypeScript 타입 시스템을 프로그래밍 언어로 활용하는 고급 기법 — 산술 연산, 문자열 파서, 상태 머신 등을 타입만으로 구현하는 패턴을 다룹니다.