제네릭 심화 — 조건부 분배, 재귀 타입, 타입 레벨 산술
제네릭 심화에서는 조건부 타입의 분배 동작, 재귀 타입, 그리고 타입 레벨에서 연산을 수행하는 방법을 다룹니다.
조건부 분배 심화
// 유니온 + 조건부 타입 = 분배
type Boxed<T> = T extends any ? { value: T } : never;
type Result = Boxed<string | number>;
// { value: string } | { value: number }
// ⚠️ { value: string | number }가 아님!
분배 제어하기
// 분배 방지: 대괄호로 감싸기
type BoxedNonDist<T> = [T] extends [any] ? { value: T } : never;
type Result2 = BoxedNonDist<string | number>;
// { value: string | number } — 분배 없이 통째로 처리
// 분배가 필요한 경우와 아닌 경우를 구분하기
type IsUnion<T, C = T> =
T extends C
? [C] extends [T]
? false
: true
: never;
type A = IsUnion<string>; // false
type B = IsUnion<string | number>; // true
재귀 타입(Recursive Types)
Deep Partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
database: {
host: string;
port: number;
credentials: {
user: string;
password: string;
};
};
logging: {
level: string;
};
}
type PartialConfig = DeepPartial<Config>;
// database?.credentials?.user? — 모든 레벨이 선택적
깊은 키 경로 추출
type PathKeys<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]:
| `${Prefix}${K}`
| PathKeys<T[K], `${Prefix}${K}.`>;
}[keyof T & string]
: never;
interface User {
name: string;
address: {
city: string;
zip: number;
};
}
type UserPaths = PathKeys<User>;
// 'name' | 'address' | 'address.city' | 'address.zip'
Flatten (중첩 배열 펼치기)
type Flatten<T> = T extends (infer U)[]
? Flatten<U>
: T;
type A = Flatten<number[]>; // number
type B = Flatten<number[][]>; // number
type C = Flatten<number[][][]>; // number
type D = Flatten<string>; // string
타입 레벨 산술
TypeScript의 타입 시스템에서 숫자 연산을 구현하는 트릭입니다. 튜플의 길이를 이용합니다.
// 튜플의 길이로 숫자를 표현
type Length<T extends any[]> = T['length'];
type Zero = Length<[]>; // 0
type Three = Length<[1, 2, 3]>; // 3
// N 길이의 튜플 생성
type BuildTuple<N extends number, T extends any[] = []> =
T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>;
type FiveTuple = BuildTuple<5>; // [unknown, unknown, unknown, unknown, unknown]
// 덧셈
type Add<A extends number, B extends number> =
Length<[...BuildTuple<A>, ...BuildTuple<B>]>;
type Sum = Add<3, 4>; // 7
// 뺄셈
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
? Rest['length']
: never;
type Diff = Subtract<7, 3>; // 4
공부하다 보니 TypeScript 타입 시스템이 사실상 프로그래밍 언어 수준이더라고요. 물론 실무에서 타입 레벨 산술을 쓸 일은 드물지만, 타입 챌린지에서 자주 나옵니다.
제네릭 함수의 타입 추론 개선
// ❌ T가 너무 넓게 추론됨
function createPair<T>(a: T, b: T): [T, T] {
return [a, b];
}
// createPair(1, 'hello'); // Error — T가 number | string으로 추론 안 됨
// ✅ 각각 다른 타입 매개변수
function createPair2<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const pair = createPair2(1, 'hello'); // [number, string]
NoInfer (TS 5.4+)
// NoInfer로 특정 위치에서 타입 추론을 막기
function createFSM<S extends string>(
initial: NoInfer<S>,
states: S[]
) {
return { current: initial, states };
}
// states에서 S가 추론되고, initial은 추론에 참여하지 않음
createFSM('idle', ['idle', 'loading', 'done']); // OK
// createFSM('invalid', ['idle', 'loading', 'done']); // ❌ Error
제네릭 제약과 조건부 타입 조합
// 값이 배열이면 요소 타입을, 아니면 그대로 반환
type Unwrap<T> = T extends readonly (infer U)[] ? U : T;
function first<T>(value: T): Unwrap<T> {
if (Array.isArray(value)) {
return value[0];
}
return value as Unwrap<T>;
}
const a = first([1, 2, 3]); // number
const b = first('hello'); // string
정리
- 유니온에 조건부 타입을 적용하면 각 멤버에 분배된다 (방지:
[T] extends [U]) - 재귀 타입으로 DeepPartial, Flatten 등 깊은 구조의 변환이 가능하다
- 타입 레벨 산술은 튜플 길이를 이용해 덧셈, 뺄셈을 구현한다
NoInfer로 특정 위치에서 타입 추론을 막을 수 있다 (TS 5.4+)- 실무보다는 타입 챌린지와 라이브러리 개발에서 주로 사용되는 패턴이다
댓글 로딩 중...