iBetter Books
수정

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 타입 시스템의 핵심 빌딩 블록입니다. 내장 유틸리티 타입들이 모두 이 패턴으로 구현되어 있으며, 직접 커스텀 유틸리티 타입을 만들면 도메인에 맞는 정확한 타입 변환이 가능합니다.