7.1 infer 키워드
TypeScript를 처음 배울 때는 타입을 직접 명시하지만, 고급 타입 프로그래밍에서는 타입을 추론해서 꺼내는 기술이 필요합니다. infer 키워드는 조건부 타입 안에서 "이 자리의 타입을 변수에 담아 나중에 쓰겠다"고 TypeScript에게 알려주는 명령어입니다. 마치 퍼즐 조각을 꺼내 이름표를 붙이는 것처럼, 복잡한 타입 구조 안에서 원하는 부분만 골라낼 수 있습니다.
infer란 무엇인가
infer는 extends 조건부 타입 절 안에서만 사용할 수 있는 특수 키워드입니다. 패턴 매칭(pattern matching)과 비슷한 개념으로, 특정 타입 구조가 주어진 패턴에 맞을 때 그 패턴 안의 특정 위치 타입을 새로운 타입 변수로 캡처합니다.
// 기본 문법
type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// R이 추론되는 과정
// T = () => string 이면
// () => string extends (...args: any[]) => infer R
// => R은 string으로 추론됨
// 결과: string
infer R은 "오른쪽(반환 위치)의 타입을 R이라는 타입 변수에 담아라"는 지시입니다. 조건이 참일 때만 R을 사용할 수 있으며, 거짓인 분기에서는 R이 존재하지 않습니다.
핵심 개념
조건부 타입과 infer의 관계
infer는 반드시 조건부 타입(T extends ... ? A : B) 안에서만 사용합니다. 독립적으로 쓸 수 없습니다.
// 올바른 사용
type Correct<T> = T extends Promise<infer V> ? V : never;
// 잘못된 사용 — 컴파일 에러
// type Wrong<T> = infer V; // ❌
infer 위치의 의미
infer가 놓이는 위치에 따라 추론되는 타입이 달라집니다.
// 함수 반환 타입
type ReturnT<T> = T extends (...args: any[]) => infer R ? R : never;
// 함수 파라미터 타입 (튜플로 추론)
type ParamsT<T> = T extends (...args: infer P) => any ? P : never;
// 배열 요소 타입
type ElementT<T> = T extends (infer E)[] ? E : never;
// Promise 내부 타입
type PromiseT<T> = T extends Promise<infer V> ? V : never;
ReturnType, Parameters, ConstructorParameters 직접 구현
TypeScript 내장 유틸리티 타입들이 내부적으로 어떻게 구현되는지 직접 만들어보겠습니다.
// ReturnType<T> 직접 구현
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
function add(a: number, b: number): number {
return a + b;
}
type AddReturn = MyReturnType<typeof add>; // number
// Parameters<T> 직접 구현
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type AddParams = MyParameters<typeof add>; // [a: number, b: number]
// ConstructorParameters<T> 직접 구현
type MyConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
class User {
constructor(
public name: string,
public age: number,
public email: string
) {}
}
type UserCtorParams = MyConstructorParameters<typeof User>;
// [name: string, age: number, email: string]
// InstanceType<T> 직접 구현
type MyInstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer I ? I : never;
type UserInstance = MyInstanceType<typeof User>; // User
배열 요소 타입 추출
배열 타입에서 요소의 타입을 꺼내는 패턴입니다.
// 1차원 배열 요소 타입
type ArrayElement<T> = T extends (infer Item)[] ? Item : never;
type StringItem = ArrayElement<string[]>; // string
type NumberItem = ArrayElement<number[]>; // number
type MixedItem = ArrayElement<(string | number)[]>; // string | number
// 읽기 전용 배열도 처리
type ReadonlyArrayElement<T> =
T extends readonly (infer Item)[] ? Item : never;
type ROItem = ReadonlyArrayElement<readonly string[]>; // string
// 중첩 배열에서 가장 깊은 요소 타입 추출 (재귀)
type DeepArrayElement<T> =
T extends (infer Item)[]
? DeepArrayElement<Item>
: T;
type Nested = DeepArrayElement<string[][][]>; // string
type Flat = DeepArrayElement<number[]>; // number
type Scalar = DeepArrayElement<boolean>; // boolean
// 튜플에서 특정 위치의 타입 추출
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 TupleFirst = First<[string, number, boolean]>; // string
type TupleLast = Last<[string, number, boolean]>; // boolean
type EmptyFirst = First<[]>; // never
함수 첫 번째/마지막 파라미터 추출
// 첫 번째 파라미터 타입
type FirstParameter<T extends (...args: any) => any> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
// 마지막 파라미터 타입
type LastParameter<T extends (...args: any) => any> =
T extends (...args: infer P) => any
? P extends [...any[], infer L]
? L
: never
: never;
// 나머지 파라미터 타입 (첫 번째 제외)
type TailParameters<T extends (...args: any) => any> =
T extends (first: any, ...rest: infer R) => any ? R : never;
// 테스트
function greet(name: string, age: number, city: string): string {
return `${name}(${age}) from ${city}`;
}
type GreetFirst = FirstParameter<typeof greet>; // string
type GreetLast = LastParameter<typeof greet>; // string
type GreetTail = TailParameters<typeof greet>; // [age: number, city: string]
// 실용 예: 커링을 위한 파라미터 분리
type Curry<T extends (...args: any) => any> =
Parameters<T> extends [infer Head, ...infer Tail]
? Tail extends []
? T
: (arg: Head) => Curry<(...args: Tail) => ReturnType<T>>
: never;
프로미스 언래핑 — Awaited 원리
TypeScript 4.5부터 내장된 Awaited<T>의 원리를 직접 구현해봅니다.
// 단순 버전: 한 겹 언래핑
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type S1 = UnwrapPromise<Promise<string>>; // string
type S2 = UnwrapPromise<string>; // string (Promise 아니면 그대로)
// 재귀 버전: 중첩 Promise도 완전히 언래핑
type DeepUnwrapPromise<T> =
T extends Promise<infer V>
? DeepUnwrapPromise<V>
: T;
type Nested1 = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>; // string
type Nested2 = DeepUnwrapPromise<Promise<number[]>>; // number[]
// 내장 Awaited<T>의 실제 구현 (TypeScript lib.es5.d.ts 참고)
type MyAwaited<T> =
T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
? F extends (value: infer V, ...args: infer _) => any
? MyAwaited<V>
: never
: T;
// PromiseLike도 지원 (then 메서드만 있어도 됨)
type A1 = MyAwaited<Promise<string>>; // string
type A2 = MyAwaited<{ then: (f: (v: number) => any) => any }>; // number
type A3 = MyAwaited<string>; // string
// async 함수 반환 타입에서 실제 값 타입 추출
async function fetchUser(): Promise<{ id: number; name: string }> {
return { id: 1, name: "Alice" };
}
type FetchUserResult = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
재귀 + infer 결합
infer를 재귀 타입과 결합하면 매우 강력한 타입 변환이 가능합니다.
// 튜플을 유니온 타입으로 변환
type TupleToUnion<T extends any[]> =
T extends [infer Head, ...infer Tail]
? Head | TupleToUnion<Tail>
: never;
type Union1 = TupleToUnion<[string, number, boolean]>;
// string | number | boolean
// 튜플 뒤집기
type Reverse<T extends any[]> =
T extends [infer Head, ...infer Tail]
? [...Reverse<Tail>, Head]
: [];
type Rev1 = Reverse<[1, 2, 3]>; // [3, 2, 1]
type Rev2 = Reverse<[string, number]>; // [number, string]
// 함수 체인의 최종 반환 타입 추론
type ChainReturn<T> =
T extends (...args: any[]) => infer R
? R extends (...args: any[]) => any
? ChainReturn<R>
: R
: T;
declare function makeAdder(x: number): (y: number) => string;
type AdderResult = ChainReturn<typeof makeAdder>; // string
// 깊은 객체에서 특정 키 경로의 타입 추출
type GetPath<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetPath<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
app: {
name: string;
version: string;
};
}
type DbHost = GetPath<Config, "database.host">; // string
type DbPort = GetPath<Config, "database.port">; // number
type DbUser = GetPath<Config, "database.credentials.username">; // string
type AppName = GetPath<Config, "app.name">; // string
실전 예제 1: 미들웨어 체인 타입 추론
Express 스타일의 미들웨어 체인에서 컨텍스트 타입을 자동으로 추론합니다.
// 미들웨어 타입 정의
type Middleware<TIn, TOut> = (
ctx: TIn,
next: (ctx: TOut) => void
) => void;
// 미들웨어 체인에서 최종 컨텍스트 타입 추론
type ExtractMiddlewareOutput<T> =
T extends Middleware<any, infer Out> ? Out : never;
type ExtractMiddlewareInput<T> =
T extends Middleware<infer In, any> ? In : never;
// 컨텍스트를 단계적으로 확장하는 미들웨어 체인
interface BaseCtx {
requestId: string;
timestamp: Date;
}
interface AuthCtx extends BaseCtx {
userId: string;
roles: string[];
}
interface ValidatedCtx extends AuthCtx {
body: unknown;
params: Record<string, string>;
}
// 각 미들웨어의 타입이 자동으로 추론됨
const authMiddleware: Middleware<BaseCtx, AuthCtx> = (ctx, next) => {
next({ ...ctx, userId: "user-123", roles: ["admin"] });
};
const validationMiddleware: Middleware<AuthCtx, ValidatedCtx> = (ctx, next) => {
next({ ...ctx, body: {}, params: {} });
};
type AuthOutput = ExtractMiddlewareOutput<typeof authMiddleware>; // AuthCtx
type ValidInput = ExtractMiddlewareInput<typeof validationMiddleware>; // AuthCtx
// 파이프라인 타입 - 미들웨어 배열의 최종 출력 타입
type PipelineOutput<T extends Middleware<any, any>[]> =
T extends [...any[], infer Last]
? ExtractMiddlewareOutput<Last>
: never;
type Pipeline = [typeof authMiddleware, typeof validationMiddleware];
type FinalCtx = PipelineOutput<Pipeline>; // ValidatedCtx
// 실제 파이프라인 실행기
function createPipeline<T extends BaseCtx>(
middlewares: Middleware<any, any>[]
): (ctx: T) => void {
return (ctx: T) => {
let index = 0;
const run = (currentCtx: any) => {
if (index < middlewares.length) {
const middleware = middlewares[index++];
middleware(currentCtx, run);
}
};
run(ctx);
};
}
실전 예제 2: 함수 조합 반환 타입 추론
함수형 프로그래밍의 compose / pipe 패턴에서 타입을 안전하게 추론합니다.
// 단일 함수 합성의 반환 타입 추론
type ComposedReturn<F extends (...args: any) => any, G extends (...args: any) => any> =
ReturnType<F> extends Parameters<G>[0]
? (...args: Parameters<F>) => ReturnType<G>
: never;
// 파이프: 왼쪽에서 오른쪽으로 실행
function pipe<A, B>(
f: (a: A) => B
): (a: A) => B;
function pipe<A, B, C>(
f: (a: A) => B,
g: (b: B) => C
): (a: A) => C;
function pipe<A, B, C, D>(
f: (a: A) => B,
g: (b: B) => C,
h: (c: C) => D
): (a: A) => D;
function pipe(...fns: Function[]) {
return (x: any) => fns.reduce((acc, fn) => fn(acc), x);
}
const transform = pipe(
(n: number) => n.toString(),
(s: string) => s.length,
(n: number) => n > 5
);
// 타입: (a: number) => boolean
// 이벤트 핸들러의 반환 타입 추론
type EventHandler<T extends Event> = (event: T) => void;
type ExtractEventType<H> = H extends EventHandler<infer E> ? E : never;
type ClickHandlerEvent = ExtractEventType<EventHandler<MouseEvent>>; // MouseEvent
// API 응답 타입 추론
type ApiFunction = (...args: any[]) => Promise<any>;
type ApiResponse<T extends ApiFunction> =
Awaited<ReturnType<T>>;
async function getUser(id: string): Promise<{ id: string; name: string; email: string }> {
// API 호출
return { id, name: "Alice", email: "alice@example.com" };
}
async function getUserList(): Promise<Array<{ id: string; name: string }>> {
return [];
}
type SingleUser = ApiResponse<typeof getUser>;
// { id: string; name: string; email: string }
type UserList = ApiResponse<typeof getUserList>;
// { id: string; name: string }[]
고수 팁
infer 위치 규칙: 공변 vs 반공변
infer가 놓이는 위치(공변/반공변)에 따라 여러 infer가 같은 변수를 참조할 때 결과가 달라집니다.
// 공변 위치(covariant): 유니온 타입으로 합쳐짐
type CovariantInfer<T> =
T extends { a: infer U; b: infer U } ? U : never;
type C1 = CovariantInfer<{ a: string; b: number }>;
// string | number ← 유니온
// 반공변 위치(contravariant): 인터섹션 타입으로 합쳐짐
type ContravariantInfer<T> =
T extends {
fn: (x: infer U) => void;
fn2: (x: infer U) => void;
}
? U
: never;
type F1 = ContravariantInfer<{
fn: (x: string) => void;
fn2: (x: number) => void;
}>;
// string & number = never ← 인터섹션
여러 infer 동시 사용
한 조건부 타입 안에서 여러 infer 변수를 동시에 사용할 수 있습니다.
// 함수의 첫 번째 파라미터와 반환 타입을 동시에 추출
type FirstParamAndReturn<T> =
T extends (first: infer P, ...rest: any[]) => infer R
? { param: P; return: R }
: never;
function process(id: number, data: string): boolean {
return true;
}
type ProcessInfo = FirstParamAndReturn<typeof process>;
// { param: number; return: boolean }
// 객체 타입에서 키-값 쌍을 동시에 추출
type KeyValuePair<T> =
T extends { [K in infer Key extends string]: infer Val }
? { key: Key; value: Val }
: never;
// Map 타입 변환 유틸리티
type MapTuple<T extends [any, any][]> = {
[K in T[number] as K[0]]: K[1];
};
type MyMap = MapTuple<[
["name", string],
["age", number],
["active", boolean]
]>;
// { name: string; age: number; active: boolean }
infer 디버깅 팁
// infer로 추론된 타입을 확인하는 디버그 유틸리티
type Inspect<T> = T extends infer U ? U : never;
// 복잡한 타입 중간 확인
type ComplexType = Inspect<
ReturnType<typeof JSON.parse>
>; // any
// 타입이 예상과 다를 때 분해해서 확인
type Debug<T, Label extends string> = {
[K in Label]: T
};
type CheckResult = Debug<
ReturnType<typeof fetch>,
"FetchResult"
>; // { FetchResult: Promise<Response> }
정리
| 패턴 | 문법 | 용도 |
|---|---|---|
| 반환 타입 추출 | T extends (...) => infer R | 함수 반환 타입 |
| 파라미터 추출 | T extends (...args: infer P) => any | 함수 파라미터 튜플 |
| 배열 요소 추출 | T extends (infer E)[] | 배열/튜플 요소 타입 |
| Promise 언래핑 | T extends Promise<infer V> | 비동기 값 타입 |
| 생성자 파라미터 | T extends new (...args: infer P) => any | 클래스 생성자 파라미터 |
| 재귀 infer | infer + 재귀 조건부 타입 | 깊은 구조 변환 |
| 공변 위치 | 반환값, 객체 프로퍼티 값 | 유니온으로 병합 |
| 반공변 위치 | 함수 파라미터 | 인터섹션으로 병합 |
다음 장에서는 Awaited, ConstructorParameters, InstanceType, ThisType 등 TypeScript가 제공하는 고급 내장 유틸리티 타입을 심층적으로 살펴보고, 실전 패턴에서 어떻게 조합하는지 알아봅니다.