재귀 조건부 타입 — 타입 레벨 패턴 매칭의 극한
TypeScript 재귀 조건부 타입으로 중첩 배열 평탄화, 튜플 조작, 문자열 치환, UnionToIntersection, 순열 생성까지 타입 레벨 알고리즘을 완전히 정리합니다.
지난 글에서 재귀 타입의 기본 구조를 살펴봤다. 이번에는 재귀 조건부 타입으로 한 단계 더 나아간다. 조건부 타입의 infer와 재귀를 결합하면 런타임 함수처럼 타입을 분해하고 재조합하는 “타입 레벨 알고리즘”을 만들 수 있다. 중첩 배열 평탄화, 문자열 치환, 유니언-교차 변환이 대표적인 예다.
Flatten — 중첩 배열 평탄화
type Flatten<T> =
T extends (infer Item)[]
? Flatten<Item> // Item이 배열이면 계속 벗기기
: T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string[]>; // string
type C = Flatten<string>; // string (배열 아님, 즉시 반환)
T extends (infer Item)[]로 T가 배열인지 확인하고, 배열이면 요소 타입 Item으로 재귀한다. 배열이 아닐 때 T를 반환하면 종료 조건이 된다.
문자열 치환 — Replace
type Replace<
S extends string,
From extends string,
To extends string
> = S extends `${infer A}${From}${infer B}`
? Replace<`${A}${To}${B}`, From, To>
: S;
type R1 = Replace<"a_b_c", "_", "-">; // "a-b-c"
type R2 = Replace<"hello world", " ", "_">; // "hello_world"
${infer A}${From}${infer B} 패턴으로 문자열을 분해하고, From을 To로 교체한 뒤 결과에 재귀를 적용해 모든 출현을 치환한다.
Trim 타입
type TrimLeft<S extends string> =
S extends ` ${infer R}` ? TrimLeft<R> : S;
type TrimRight<S extends string> =
S extends `${infer L} ` ? TrimRight<L> : S;
type Trim<S extends string> = TrimRight<TrimLeft<S>>;
type T1 = Trim<" hello ">; // "hello"
UnionToIntersection — 유니언을 교차 타입으로
반변(contravariant) 위치에서 infer하면 여러 후보가 교차 타입으로 합쳐지는 TypeScript 동작을 이용한다.
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;
type UI1 = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }
type UI2 = UnionToIntersection<string | number>;
// string & number (= never)
U extends any로 분배해 각 멤버를 (k: U) => void로 만든 뒤, 다시 extends (k: infer I) => void로 합치면 반변 위치 infer가 교차로 통합한다.
튜플 조작
재귀와 variadic 튜플 타입을 결합하면 튜플을 배열처럼 처리할 수 있다.
// 첫 요소 제거
type Tail<T extends unknown[]> =
T extends [unknown, ...infer R] ? R : never;
type T1 = Tail<[1, 2, 3]>; // [2, 3]
type T2 = Tail<[string]>; // []
// 튜플 뒤집기
type Reverse<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer H, ...infer Rest]
? Reverse<Rest, [H, ...Acc]>
: Acc;
type Rev = Reverse<[1, 2, 3]>; // [3, 2, 1]
어큐뮬레이터 패턴(Acc extends unknown[] = [])은 꼬리 재귀를 모방해 더 깊은 재귀를 가능하게 한다.
깊이 제한과 카운터 패턴
TypeScript는 재귀 깊이 약 50에서 오류를 낸다. 깊이를 튜플 길이로 측정하는 패턴을 쓰면 이를 제어할 수 있다.
type Repeat<
T,
N extends number,
Acc extends T[] = []
> = Acc["length"] extends N ? Acc : Repeat<T, N, [T, ...Acc]>;
type R3 = Repeat<string, 3>; // [string, string, string]
// 더하기 타입 (type-level arithmetic에서 자세히)
type Add<A extends number, B extends number> =
[...Repeat<unknown, A>, ...Repeat<unknown, B>]["length"];
type Sum = Add<3, 4>; // 7
실전 패턴: 객체 경로 배열
type PathArray<T, Prefix extends PropertyKey[] = []> =
T extends object
? {
[K in keyof T]:
| [...Prefix, K]
| PathArray<T[K], [...Prefix, K]>
}[keyof T]
: Prefix;
type Config = { db: { host: string; port: number }; cache: { ttl: number } };
type AllPaths = PathArray<Config>;
// ["db"] | ["db", "host"] | ["db", "port"] | ["cache"] | ["cache", "ttl"]
핵심 정리
재귀 조건부 타입은 infer로 타입을 분해하고 재귀 호출로 반복을 표현하는 “타입 레벨 함수”다. 종료 조건을 항상 명확히 두어야 하고, 깊이 제한(~50)에 걸릴 때는 어큐뮬레이터 패턴으로 우회한다. 분배 조건부 타입과 조합하면 유니언 조작, 교차 변환, 순열 생성 같은 고급 패턴이 가능하다.
지난 글: 재귀 타입 — 자기 자신을 참조하는 타입 구조
다음 글: 타입 레벨 산술 — 튜플 길이로 숫자 연산 구현
읽어주셔서 감사합니다. 😊