Ch 05. 매핑 타입과 유틸리티 타입
TypeScript에는 Partial, Required, Pick, Omit, Record 같은 유틸리티 타입이 내장되어 있습니다. 이것들이 어떻게 구현되어 있는지 이해하면 직접 커스텀 유틸리티 타입을 만들 수 있습니다. 그 기반이 되는 것이 매핑 타입(mapped types)입니다.
매핑 타입의 기본 문법
매핑 타입은 기존 타입의 모든 키를 순회하며 새 타입을 만듭니다.
// 파일: src/generics/mapped-types.ts// { [K in keyof T]: 변환 }// 모든 속성을 optional로 만들기type MyPartial<T> = { [K in keyof T]?: T[K];};// 모든 속성을 required로 만들기type MyRequired<T> = { [K in keyof T]-?: T[K]; // -?는 optional 제거};// 모든 속성을 readonly로 만들기type MyReadonly<T> = { readonly [K in keyof T]: T[K];};// 모든 readonly 제거type Mutable<T> = { -readonly [K in keyof T]: T[K];};interface User { id: number; name: string; email?: string;}type PartialUser = MyPartial<User>;// { id?: number; name?: string; email?: string }type RequiredUser = MyRequired<User>;// { id: number; name: string; email: string }type ReadonlyUser = MyReadonly<User>;// { readonly id: number; readonly name: string; readonly email?: string }
Pick과 Omit 구현하기
// 파일: src/generics/pick-omit.ts// Pick: 특정 키만 선택type MyPick<T, K extends keyof T> = { [P in K]: T[P];};// Omit: 특정 키를 제외type MyOmit<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P];};interface Product { id: number; title: string; price: number; stock: number; description: string;}type ProductSummary = MyPick<Product, "id" | "title" | "price">;// { id: number; title: string; price: number }type ProductWithoutStock = MyOmit<Product, "stock">;// { id: number; title: string; price: number; description: string }// 실전 활용 — API 요청/응답에서 ID 제거type CreateProductRequest = Omit<Product, "id">;// id 없이 나머지 모든 필드type UpdateProductRequest = Partial<Omit<Product, "id">>;// id 없이 나머지 모두 optional
Record 구현하기
// 파일: src/generics/record.ts// Record: 키와 값의 타입을 지정하는 객체 타입type MyRecord<K extends keyof any, V> = { [P in K]: V;};// 사용 예시type StatusMap = MyRecord<"active" | "inactive" | "pending", number>;// { active: number; inactive: number; pending: number }type Config = MyRecord<string, string | number | boolean>;// { [x: string]: string | number | boolean }// 실전: 에러 코드 맵const errorMessages: Record<number, string> = { 400: "잘못된 요청입니다", 401: "인증이 필요합니다", 403: "권한이 없습니다", 404: "리소스를 찾을 수 없습니다", 500: "서버 오류가 발생했습니다",};// 실전: 설정 객체 타입type Environment = "development" | "staging" | "production";const dbConfig: Record<Environment, { host: string; port: number }> = { development: { host: "localhost", port: 5432 }, staging: { host: "staging.db.example.com", port: 5432 }, production: { host: "prod.db.example.com", port: 5432 },};
값 타입 변환 매핑
// 파일: src/generics/value-mapping.ts// 모든 값을 특정 타입으로 변환type Stringify<T> = { [K in keyof T]: string;};// Promise로 감싸기type Promisify<T> = { [K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T[K];};interface SyncService { getUser(id: number): { id: number; name: string }; listPosts(): { id: number; title: string }[]; deletePost(id: number): boolean;}type AsyncService = Promisify<SyncService>;// {// getUser(id: number): Promise<{ id: number; name: string }>;// listPosts(): Promise<{ id: number; title: string }[]>;// deletePost(id: number): Promise<boolean>;// }
키 재매핑 (Key Remapping)
TypeScript 4.1부터 as 절로 매핑 시 키를 변환할 수 있습니다.
// 파일: src/generics/key-remapping.ts// getter 메서드 이름 생성type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};interface Config { host: string; port: number; debug: boolean;}type ConfigGetters = Getters<Config>;// {// getHost: () => string;// getPort: () => number;// getDebug: () => boolean;// }// 특정 키 필터링 (falsy 키는 제거됨)type NonNullableProperties<T> = { [K in keyof T as T[K] extends null | undefined ? never : K]: T[K];};interface MaybeUser { id: number; name: string | null; email: string | undefined; age: number;}type DefiniteUser = NonNullableProperties<MaybeUser>;// { id: number; age: number }
자주 쓰는 커스텀 유틸리티 타입 모음
// 파일: src/generics/utility-collection.ts// 특정 키만 Required로 만들기type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;// 특정 키만 Partial로 만들기type OptionalFields<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;// 중첩 객체도 Partial로 만들기type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;// 중첩 객체도 Readonly로 만들기type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;// 사용 예시interface Article { id: number; title: string; content: string; author: { id: number; name: string; }; tags: string[];}// id와 title은 필수, 나머지는 optionaltype CreateArticle = OptionalFields<Article, "id" | "tags">;// 중첩도 모두 수정 불가type FrozenArticle = DeepReadonly<Article>;// frozen.author.name = "new"; // 오류 — readonly
매핑 타입은 TypeScript 타입 시스템의 핵심 빌딩 블록입니다. 내장 유틸리티 타입들이 모두 이 패턴으로 구현되어 있으며, 직접 커스텀 유틸리티 타입을 만들면 도메인에 맞는 정확한 타입 변환이 가능합니다.