TypeScript 고급 타입이란?
TypeScript의 타입 시스템은 단순한 타입 체크를 넘어 타입 레벨 프로그래밍이 가능한 튜링 완전 시스템입니다. Conditional Type, Mapped Type, Template Literal Type, infer 키워드를 조합하면 런타임 코드 없이도 복잡한 타입 변환과 검증을 수행할 수 있습니다.
이 글에서는 실전에서 자주 쓰이는 고급 타입 패턴, Utility Type 내부 구현, Conditional Type과 infer의 동작 원리, 그리고 NestJS/Express에서의 타입 안전 패턴까지 심층적으로 다룹니다. NestJS Custom Decorator 심화와 함께 읽으면 타입 안전한 NestJS 애플리케이션 설계를 이해할 수 있습니다.
Conditional Type + infer
Conditional Type은 타입 레벨의 if-else이고, infer는 패턴 매칭으로 타입을 추출합니다.
// 기본 Conditional Type
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// infer: 타입 패턴에서 일부를 추출
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number) => string;
type Result = ReturnType<Fn>; // string
// 함수 파라미터 추출
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = Parameters<(a: string, b: number) => void>;
// [a: string, b: number]
// Promise 내부 타입 추출 (재귀)
type UnwrapPromise<T> = T extends Promise<infer U>
? UnwrapPromise<U> // 중첩 Promise도 재귀적으로 풀기
: T;
type Deep = UnwrapPromise<Promise<Promise<string>>>; // string
// 배열 요소 타입 추출
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Item = ElementOf<string[]>; // string
// 문자열 패턴 매칭
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params2 = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'
Mapped Type 심화
// 기본 Mapped Type
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
// Key Remapping (4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
// 특정 키 필터링
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringProps = OnlyStrings<User>;
// { name: string } (age는 number이므로 제외)
// 재귀적 DeepPartial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
db: { host: string; port: number; ssl: { enabled: boolean } };
cache: { ttl: number };
}
type PartialConfig = DeepPartial<Config>;
// db?.host?, db?.ssl?.enabled? 모두 선택적
// Mutable (readonly 제거)
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// Required (? 제거)
type Required<T> = { [K in keyof T]-?: T[K] };
Template Literal Type
// 문자열 패턴 타입
type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// HTTP 메서드 + 경로 조합
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = '/users' | '/posts' | '/comments';
type Endpoint = `${Method} ${Route}`;
// 'GET /users' | 'GET /posts' | ... (12가지 조합)
// CSS 단위 타입
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = '100px'; // ✅
const bad: CSSValue = '100'; // ❌ 컴파일 에러
// 문자열 분할·조합
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Parts = Split<'a.b.c', '.'>;
// ['a', 'b', 'c']
// 깊은 경로 타입 (dotted path)
type DeepKeyOf<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]: K | `${K}.${DeepKeyOf<T[K]>}`
}[keyof T & string]
: never;
type Paths = DeepKeyOf<{ a: { b: { c: number }; d: string } }>;
// 'a' | 'a.b' | 'a.b.c' | 'a.d'
Utility Type 내부 구현
// Pick: 특정 키만 선택
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Omit: 특정 키 제외
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// Exclude: 유니언에서 특정 타입 제거
type Exclude<T, U> = T extends U ? never : T;
// Exclude<'a' | 'b' | 'c', 'a'> → 'b' | 'c'
// 분배 법칙: 'a' extends 'a' → never | 'b' extends 'a' → 'b' | ...
// Extract: 유니언에서 특정 타입만 추출
type Extract<T, U> = T extends U ? T : never;
// NonNullable: null | undefined 제거
type NonNullable<T> = T & {}; // 또는 Exclude<T, null | undefined>
// Record: 키-값 매핑
type Record<K extends keyof any, T> = { [P in K]: T };
// Awaited (4.5+): Promise 재귀 언래핑
type Awaited<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
? F extends (value: infer V, ...args: infer _) => any
? Awaited<V>
: never
: T;
실전 패턴: 타입 안전 API
// 패턴 1: 타입 안전 이벤트 이미터
type EventMap = {
userCreated: { userId: string; email: string };
orderPlaced: { orderId: string; amount: number };
paymentFailed: { orderId: string; reason: string };
};
class TypedEmitter<T extends Record<string, any>> {
private handlers = new Map<string, Function[]>();
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
const list = this.handlers.get(event as string) || [];
list.push(handler);
this.handlers.set(event as string, list);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
this.handlers.get(event as string)?.forEach(h => h(payload));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('userCreated', (e) => {
console.log(e.userId); // ✅ 타입 추론됨
console.log(e.amount); // ❌ 컴파일 에러!
});
// 패턴 2: Builder 패턴 타입 추적
class QueryBuilder<Selected extends string = never> {
select<T extends string>(...columns: T[]): QueryBuilder<Selected | T> {
return this as any;
}
execute(): Promise<Record<Selected, unknown>[]> {
return Promise.resolve([]);
}
}
const result = await new QueryBuilder()
.select('name', 'age')
.execute();
// result: Record<'name' | 'age', unknown>[]
// result[0].name ✅
// result[0].email ❌ 컴파일 에러
// 패턴 3: 타입 안전 라우터 (NestJS/Express)
type RouteHandler<
Params extends Record<string, string> = {},
Body = unknown,
Response = unknown
> = (req: { params: Params; body: Body }) => Promise<Response>;
const getUser: RouteHandler<{ userId: string }, never, User> =
async (req) => {
const { userId } = req.params; // string 타입 보장
return findUser(userId);
};
Type Guard와 Narrowing
// 사용자 정의 타입 가드
interface Dog { bark(): void; breed: string }
interface Cat { meow(): void; color: string }
type Pet = Dog | Cat;
function isDog(pet: Pet): pet is Dog {
return 'bark' in pet;
}
function handlePet(pet: Pet) {
if (isDog(pet)) {
pet.bark(); // ✅ Dog로 좁혀짐
pet.breed; // ✅
} else {
pet.meow(); // ✅ Cat으로 좁혀짐
}
}
// Discriminated Union (판별 유니언)
type ApiResponse =
| { status: 'success'; data: User; timestamp: number }
| { status: 'error'; error: string; code: number }
| { status: 'loading' };
function handleResponse(res: ApiResponse) {
switch (res.status) {
case 'success':
console.log(res.data); // ✅ data 접근 가능
break;
case 'error':
console.log(res.error); // ✅ error 접근 가능
break;
case 'loading':
// res.data ❌ 컴파일 에러
break;
}
}
// satisfies 연산자 (4.9+)
const config = {
port: 3000,
host: 'localhost',
debug: true,
} satisfies Record<string, string | number | boolean>;
// config.port의 타입: number (3000이 아닌 number로 추론)
// Record 제약은 만족하면서도 각 속성의 구체 타입 유지
NestJS Zod 스키마 검증 심화에서 런타임 타입 검증과의 조합도 확인하세요.
정리
TypeScript 고급 타입은 Conditional Type(타입 레벨 조건문), Mapped Type(타입 레벨 반복문), Template Literal Type(문자열 패턴), infer(패턴 매칭 추출)이 4대 축입니다. 이를 조합하면 API 응답 타입 자동 생성, 이벤트 이미터 타입 안전성, 라우터 파라미터 추출 등을 컴파일 타임에 보장할 수 있습니다. Utility Type의 내부 구현을 이해하면 프로젝트에 맞는 커스텀 타입을 자유롭게 만들 수 있습니다.