TypeScript 타입 시스템을 프로그래밍 언어로 활용하는 고급 기법 — 산술 연산, 문자열 파서, 상태 머신 등을 타입만으로 구현하는 패턴을 다룹니다.
9장까지 조건부 타입, 매핑 타입, 템플릿 리터럴 타입, infer, NoInfer를 개별적으로 학습했습니다. 이 장에서는 이 모든 도구를 결합하여 타입 수준에서 로직을 작성하는 고급 기법을 다룹니다. TypeScript의 타입 시스템은 튜링 완전(Turing Complete)하기 때문에, 이론적으로 어떤 계산이든 타입 수준에서 수행할 수 있습니다.
이 장의 내용은 "타입 체조(Type Gymnastics)"라고도 불리는 고급 기법입니다. 실무에서 이 수준의 복잡한 타입을 직접 작성할 일은 흔하지 않지만, 이 기법들을 이해하면 타입 시스템의 한계와 가능성을 파악하고, 라이브러리의 복잡한 타입을 읽고 디버깅하는 능력이 크게 향상됩니다.
TypeScript에는 타입 수준 산술 연산자가 없지만, 튜플의 길이�� 이용하면 자연수 연산을 구현할 수 있습니다.
// 숫자를 해당 길이의 튜플로 표현
type NumberToTuple<N extends number, T extends any[] = []> =
T["length"] extends N ? T : NumberToTuple<N, [...T, any]>;
type Three = NumberToTuple<3>; // [any, any, any]
type Five = NumberToTuple<5>; // [any, any, any, any, any]type Add<A extends number, B extends number> =
[...NumberToTuple<A>, ...NumberToTuple<B>]["length"] extends infer R extends number
? R
: never;
type Sum = Add<3, 4>; // 7
type Subtract<A extends number, B extends number> =
NumberToTuple<A> extends [...NumberToTuple<B>, ...infer Rest]
? Rest["length"]
: never;
type Diff = Subtract<7, 3>; // 4
type Neg = Subtract<3, 7>; // never (음수는 표현 불가)type GreaterThan<A extends number, B extends number> =
A extends B ? false :
NumberToTuple<A> extends [...NumberToTuple<B>, ...infer _]
? true
: false;
type GT1 = GreaterThan<5, 3>; // true
type GT2 = GreaterThan<3, 5>; // false
type GT3 = GreaterThan<3, 3>; // false
type LessThan<A extends number, B extends number> = GreaterThan<B, A>;
type LT = LessThan<3, 5>; // truetype Multiply<A extends number, B extends number, Acc extends any[] = []> =
B extends 0 ? Acc["length"] extends infer R extends number ? R : never :
NumberToTuple<B> extends [any, ...infer RestB]
? Multiply<A, RestB["length"], [...Acc, ...NumberToTuple<A>]>
: never;
type Product = Multiply<3, 4>; // 12
type Zero = Multiply<5, 0>; // 0타입 수준 산술은 TypeScript의 재귀 깊이 제한으로 인해 큰 숫자(일반적으로 1000 이상)에서는 동작하지 않습니다. 이는 학습과 제한된 범위의 실용적 목적에 적합합니다.
type And<A extends boolean, B extends boolean> =
A extends true ? B : false;
type Or<A extends boolean, B extends boolean> =
A extends true ? true : B;
type Not<A extends boolean> =
A extends true ? false : true;
type R1 = And<true, true>; // true
type R2 = And<true, false>; // false
type R3 = Or<false, true>; // true
type R4 = Not<true>; // false7장에서 배운 템플릿 리터럴 타입과 infer를 결합하면 타입 수준 문자열 파서를 구현할 수 있습니다.
type StringLength<S extends string, Acc extends any[] = []> =
S extends `${infer _}${infer Rest}`
? StringLength<Rest, [...Acc, any]>
: Acc["length"];
type Len1 = StringLength<"hello">; // 5
type Len2 = StringLength<"">; // 0
type Len3 = StringLength<"TypeScript">; // 10type TrimLeft<S extends string> =
S extends ` ${infer Rest}` | `\n${infer Rest}` | `\t${infer Rest}`
? TrimLeft<Rest>
: S;
type TrimRight<S extends string> =
S extends `${infer Rest} ` | `${infer Rest}\n` | `${infer Rest}\t`
? TrimRight<Rest>
: S;
type Trim<S extends string> = TrimLeft<TrimRight<S>>;
type Trimmed = Trim<" hello world ">; // "hello world"type Join<T extends string[], D extends string> =
T extends [] ? "" :
T extends [infer First extends string] ? First :
T extends [infer First extends string, ...infer Rest extends string[]]
? `${First}${D}${Join<Rest, D>}`
: never;
type Joined = Join<["a", "b", "c"], "-">; // "a-b-c"
type Path = Join<["users", "123", "posts"], "/">; // "users/123/posts"type MapSet<M extends Record<string, any>, K extends string, V> =
Omit<M, K> & Record<K, V>;
type MapGet<M extends Record<string, any>, K extends string> =
K extends keyof M ? M[K] : never;
type MapDelete<M extends Record<string, any>, K extends string> =
Omit<M, K>;
type MapHas<M extends Record<string, any>, K extends string> =
K extends keyof M ? true : false;
// 사용
type M0 = {};
type M1 = MapSet<M0, "name", "Alice">; // { name: "Alice" }
type M2 = MapSet<M1, "age", 30>; // { name: "Alice"; age: 30 }
type M3 = MapDelete<M2, "age">; // { name: "Alice" }
type Name = MapGet<M2, "name">; // "Alice"
type Has = MapHas<M2, "age">; // truetype StackPush<S extends any[], V> = [...S, V];
type StackPop<S extends any[]> =
S extends [...infer Rest, infer _] ? Rest : never;
type StackPeek<S extends any[]> =
S extends [...any[], infer Last] ? Last : never;
type StackIsEmpty<S extends any[]> =
S extends [] ? true : false;
type S0 = [];
type S1 = StackPush<S0, "a">; // ["a"]
type S2 = StackPush<S1, "b">; // ["a", "b"]
type S3 = StackPush<S2, "c">; // ["a", "b", "c"]
type Top = StackPeek<S3>; // "c"
type S4 = StackPop<S3>; // ["a", "b"]
type Empty = StackIsEmpty<S0>; // true지금까지 배운 기법을 결합하여 컴파일 타임에 유효한 상태 전이만 허용하는 상태 머신을 구현합니다.
interface StateMachineDefinition {
states: Record<string, {
on: Record<string, string>;
}>;
initial: string;
}
type ValidTransition<
Def extends StateMachineDefinition,
CurrentState extends keyof Def["states"],
Event extends string
> = Event extends keyof Def["states"][CurrentState]["on"]
? Def["states"][CurrentState]["on"][Event]
: never;
type OrderMachine = {
states: {
draft: { on: { SUBMIT: "pending"; DELETE: "cancelled" } };
pending: { on: { APPROVE: "confirmed"; REJECT: "draft" } };
confirmed: { on: { SHIP: "shipped"; CANCEL: "cancelled" } };
shipped: { on: { DELIVER: "delivered" } };
delivered: { on: {} };
cancelled: { on: {} };
};
initial: "draft";
};
// 유효한 전이
type T1 = ValidTransition<OrderMachine, "draft", "SUBMIT">; // "pending"
type T2 = ValidTransition<OrderMachine, "pending", "APPROVE">; // "confirmed"
// 유효하지 않은 전이
type T3 = ValidTransition<OrderMachine, "draft", "SHIP">; // never
type T4 = ValidTransition<OrderMachine, "delivered", "SUBMIT">; // never이를 런타임 코드와 결합하면 컴파일 타임에 잘못된 상태 전이를 방지할 수 있습니다.
class TypeSafeStateMachine<Def extends StateMachineDefinition> {
private currentState: keyof Def["states"];
constructor(private definition: Def) {
this.currentState = definition.initial as keyof Def["states"];
}
transition<
S extends keyof Def["states"],
E extends keyof Def["states"][S]["on"]
>(
_fromState: S,
event: E
): Def["states"][S]["on"][E] {
const nextState = (this.definition.states as any)[_fromState].on[event];
this.currentState = nextState;
return nextState as any;
}
getState() {
return this.currentState;
}
}type BuilderState = Record<string, unknown>;
class TypedBuilder<
Target extends Record<string, unknown>,
Built extends Partial<Target> = {}
> {
private data: Partial<Target> = {};
set<K extends Exclude<keyof Target, keyof Built>>(
key: K,
value: Target[K]
): TypedBuilder<Target, Built & Pick<Target, K>> {
(this.data as any)[key] = value;
return this as any;
}
// build()는 모든 필수 프로퍼티가 설정된 후에만 호출 가능
build(
this: TypedBuilder<Target, Target>
): Target {
return this.data as Target;
}
}
interface UserConfig {
name: string;
email: string;
role: "admin" | "user";
}
const user = new TypedBuilder<UserConfig>()
.set("name", "Alice")
.set("email", "alice@example.com")
.set("role", "admin")
.build(); // OK — 모든 프로퍼티 설정됨
// const incomplete = new TypedBuilder<UserConfig>()
// .set("name", "Alice")
// .build(); // Error — email과 role이 누락됨type JsonSchema =
| { type: "string" }
| { type: "number" }
| { type: "boolean" }
| { type: "null" }
| { type: "array"; items: JsonSchema }
| { type: "object"; properties: Record<string, JsonSchema>; required?: string[] };
type SchemaToType<S extends JsonSchema> =
S extends { type: "string" } ? string :
S extends { type: "number" } ? number :
S extends { type: "boolean" } ? boolean :
S extends { type: "null" } ? null :
S extends { type: "array"; items: infer I extends JsonSchema } ? SchemaToType<I>[] :
S extends { type: "object"; properties: infer P extends Record<string, JsonSchema>; required: infer R extends string[] }
? { [K in Extract<keyof P, R[number]>]: SchemaToType<P[K]> } &
{ [K in Exclude<keyof P, R[number]>]?: SchemaToType<P[K]> }
: S extends { type: "object"; properties: infer P extends Record<string, JsonSchema> }
? { [K in keyof P]?: SchemaToType<P[K]> }
: never;
// 스키마 정의
const userSchema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
tags: { type: "array", items: { type: "string" } },
},
required: ["name", "age"],
} as const satisfies JsonSchema;
type User = SchemaToType<typeof userSchema>;
// { name: string; age: number } & { tags?: string[] }TypeScript는 최대 재귀 깊이를 제한합니다 (보통 50~100 수준). 이를 초과하면 "Type instantiation is excessively deep and possibly infinite" 에러가 발생합니다.
유니온 타입의 멤버 수에도 제한이 있습니다. 템플릿 리터럴 타입의 조합이 너무 크면 에러가 발생합니다.
복잡한 타입 연산은 컴파일 시간을 크게 증가시킬 수 있습니다.
// 피해야 할 패턴: 과도하게 복잡한 타입
type OverlyComplex<T> =
DeepPartial<DeepReadonly<ConditionalMapping<TemplateTransform<T>>>>;
// 권장: 중간 타입으로 분해
type Step1<T> = TemplateTransform<T>;
type Step2<T> = ConditionalMapping<Step1<T>>;
type Step3<T> = DeepReadonly<Step2<T>>;
type FinalType<T> = DeepPartial<Step3<T>>;타입 수준 프로그래밍은 강력하지만, "할 수 있다"와 "해야 한다"는 다���니다. 실무에서는 타입의 복잡도와 컴파일 성능 사이의 균형을 찾는 것이 중요합니다. 복잡한 타입은 이해하기 어렵고, 에러 메시지도 난해해집니다. 가능한 한 간단한 타입으로 충분한 안전성을 확보하세요.
TypeScript의 타입 시스템은 조건부 타입, 매핑 타입, 템플릿 리터럴 타입, infer의 조합을 통해 튜링 완전한 프로그래밍 언어로 기능합니다. 산술 연산, 문자열 처리, 데이터 구조, 상태 머신 등을 타입 수준에서 구현할 수 있습니다. 이 지식은 복잡한 라이브러리 타입을 이해하고 디버깅하는 데 필수적이며, 타입 안전한 API 설계의 기반이 됩니다.
다음 장에서는 지금까지 배운 타입 시스템 지식을 실제 프로젝트에 적용합니다. tsconfig.json 최적화, 프로젝트 참조(Project References), Isolated Declarations, 모노레포 타입 전략을 다룹니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
TypeScript 프로젝트의 tsconfig.json 최적화, 프로젝트 참조, Isolated Declarations, 모노레포에서의 타입 전략을 실전 중심으로 다룹니다.
TypeScript 5.4의 NoInfer 유틸리티 타입과 5.x에서 추가된 새로운 타입 도구들을 활용한 라이브러리 설계 패턴을 다룹니다.
TypeScript 5.x의 고급 타입 기법을 총동원하여 타입 안전 유틸리티 라이브러리를 처음부터 설계하고 구현하는 실전 프로젝트입니다.