본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 6장: 매핑 타입과 키 재매핑 고급 패턴
2026년 2월 1일·프로그래밍·

6장: 매핑 타입과 키 재매핑 고급 패턴

TypeScript 매핑 타입의 원리, 수정자 조작, 키 재매핑(as 절), 그리고 조건부 타입과의 결합 패턴을 실전 예제와 함께 다룹니다.

12분772자7개 섹션
typescriptperformancedevtools
공유
typescript-deepdive6 / 12
123456789101112
이전5장: 조건부 타입 심층 분석다음7장: 템플릿 리터럴 타입으로 문자열 다루기

5장에서 조건부 타입으로 타입 수준의 조건 분기를 다뤘습니다. 이 장에서는 기존 타입을 기반으로 새로운 타입을 생성하는 매핑 타입(Mapped Types) 을 심층 분석합니다. 매핑 타입은 객체 타입의 각 프로퍼티를 순회하며 변환하는 메커니즘으로, TypeScript의 내장 유틸리티 타입 대부분이 이를 기반으로 구현되어 있습니다.

매핑 타입의 기본

매핑 타입은 in 키워드로 키를 순회하며 새로운 타입을 생성합니다.

매핑 타입 기본 구문
typescript
type MappedType<T> = {
  [K in keyof T]: TransformedType;
};

TypeScript의 내장 유틸리티 타입들이 매핑 타입으로 구현된 예를 살펴봅시다.

내장 유틸리티 타입의 매핑 타입 구현
typescript
// Readonly<T> — 모든 프로퍼티를 readonly로
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
 
// Partial<T> — 모든 프로퍼티를 선택적으로
type Partial<T> = {
  [K in keyof T]?: T[K];
};
 
// Required<T> — 모든 프로퍼티를 필수로
type Required<T> = {
  [K in keyof T]-?: T[K];
};
 
// Pick<T, K> — 특정 프로퍼티만 선택
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

수정자 조작

매핑 타입에서 readonly와 ? 수정자를 추가하거나 제거할 수 있습니다.

수정자 추가/제거
typescript
interface User {
  readonly id: string;
  name?: string;
  email?: string;
}
 
// readonly 제거
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};
 
type MutableUser = Mutable<User>;
// { id: string; name?: string; email?: string }
 
// ? 제거
type Required<T> = {
  [K in keyof T]-?: T[K];
};
 
type RequiredUser = Required<User>;
// { readonly id: string; name: string; email: string }
 
// 둘 다 제거
type MutableRequired<T> = {
  -readonly [K in keyof T]-?: T[K];
};
 
type FullUser = MutableRequired<User>;
// { id: string; name: string; email: string }

키 재매핑 (as 절)

TypeScript 4.1에서 도입된 키 재매핑(Key Remapping) 은 as 절을 사용하여 매핑 과정에서 키 이름을 변환합니다. 이 기능은 매핑 타입의 표현력을 크게 확장합니다.

키 재매핑 기본
typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
interface Person {
  name: string;
  age: number;
  email: string;
}
 
type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

키 필터링

as 절에서 never를 반환하면 해당 키가 제외됩니다. 이를 통해 조건에 따라 프로퍼티를 필터링할 수 있습니다.

키 필터링
typescript
// 함수 프로퍼티만 추출
type FunctionProperties<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};
 
// 비함수 프로퍼티만 추출
type DataProperties<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};
 
interface UserService {
  name: string;
  age: number;
  greet(): string;
  save(): Promise<void>;
}
 
type UserData = DataProperties<UserService>;
// { name: string; age: number }
 
type UserMethods = FunctionProperties<UserService>;
// { greet(): string; save(): Promise<void> }

키 변환 패턴

다양한 키 변환 패턴
typescript
// camelCase → snake_case (단순화된 버전)
type CamelToSnake<S extends string> =
  S extends `${infer C}${infer Rest}`
    ? C extends Uppercase<C>
      ? `_${Lowercase<C>}${CamelToSnake<Rest>}`
      : `${C}${CamelToSnake<Rest>}`
    : S;
 
type SnakeCase<T> = {
  [K in keyof T as CamelToSnake<string & K>]: T[K];
};
 
interface ApiResponse {
  userId: number;
  firstName: string;
  lastName: string;
  createdAt: Date;
}
 
type SnakeResponse = SnakeCase<ApiResponse>;
// {
//   user_id: number;
//   first_name: string;
//   last_name: string;
//   created_at: Date;
// }
접두사/접미사 추가
typescript
// 이벤트 핸들러 이름 생성
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    newValue: T[K],
    oldValue: T[K]
  ) => void;
};
 
interface FormState {
  username: string;
  password: string;
  remember: boolean;
}
 
type FormHandlers = EventHandlers<FormState>;
// {
//   onUsernameChange: (newValue: string, oldValue: string) => void;
//   onPasswordChange: (newValue: string, oldValue: string) => void;
//   onRememberChange: (newValue: boolean, oldValue: boolean) => void;
// }

조건부 타입과의 결합

매핑 타입과 조건부 타입을 결합하면 프로퍼티별로 다른 변환을 적용할 수 있습니다.

프로퍼티 타입에 따른 조건부 변환
typescript
// 배열 프로퍼티는 첫 요소 타입으로, 나머지는 그대로
type UnwrapArrays<T> = {
  [K in keyof T]: T[K] extends (infer U)[] ? U : T[K];
};
 
interface QueryResult {
  users: { id: string; name: string }[];
  count: number;
  page: string[];
}
 
type Unwrapped = UnwrapArrays<QueryResult>;
// {
//   users: { id: string; name: string };
//   count: number;
//   page: string;
// }

Nullable 프로퍼티 추출

선택적 프로퍼티만 추출
typescript
type OptionalKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? K : never;
}[keyof T];
 
type RequiredKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];
 
interface Config {
  host: string;
  port: number;
  ssl?: boolean;
  timeout?: number;
}
 
type OKeys = OptionalKeys<Config>;  // "ssl" | "timeout"
type RKeys = RequiredKeys<Config>;  // "host" | "port"
 
// 선택적 프로퍼티만 가진 타입
type OptionalOnly<T> = Pick<T, OptionalKeys<T>>;
type ConfigOptionals = OptionalOnly<Config>;
// { ssl?: boolean; timeout?: number }

실전 패턴

CRUD 타입 생성기

CRUD 모델 타입 자동 생성
typescript
type CreateInput<T> = Omit<T, "id" | "createdAt" | "updatedAt">;
 
type UpdateInput<T> = Partial<Omit<T, "id" | "createdAt" | "updatedAt">> & {
  id: T extends { id: infer ID } ? ID : never;
};
 
type ListResponse<T> = {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
};
 
interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "user";
  createdAt: Date;
  updatedAt: Date;
}
 
type CreateUserInput = CreateInput<User>;
// { name: string; email: string; role: "admin" | "user" }
 
type UpdateUserInput = UpdateInput<User>;
// { id: string } & { name?: string; email?: string; role?: "admin" | "user" }
 
type UserListResponse = ListResponse<User>;
// { items: User[]; total: number; page: number; pageSize: number }

타입 안전 이벤트 이미터

매핑 타입 기반 이벤트 시스템
typescript
type EventListener<T> = (payload: T) => void;
 
type EventEmitterType<Events extends Record<string, any>> = {
  on<K extends keyof Events>(event: K, listener: EventListener<Events[K]>): void;
  off<K extends keyof Events>(event: K, listener: EventListener<Events[K]>): void;
  emit<K extends keyof Events>(event: K, payload: Events[K]): void;
};
 
interface AppEvents {
  userLogin: { userId: string; timestamp: Date };
  userLogout: { userId: string };
  pageView: { path: string; referrer?: string };
  error: { code: number; message: string };
}
 
// 사용
declare const emitter: EventEmitterType<AppEvents>;
 
emitter.on("userLogin", (payload) => {
  // payload: { userId: string; timestamp: Date } — 자동 추론
  console.log(payload.userId);
});
 
emitter.emit("error", { code: 500, message: "Server error" }); // OK
emitter.emit("error", { code: "500" }); // Error: string은 number에 할당 불가

깊은 Partial

재귀적 DeepPartial
typescript
type DeepPartial<T> = T extends object
  ? T extends (...args: any[]) => any
    ? T
    : { [K in keyof T]?: DeepPartial<T[K]> }
  : T;
 
interface AppConfig {
  server: {
    port: number;
    host: string;
    cors: {
      origin: string[];
      credentials: boolean;
    };
  };
  database: {
    url: string;
    pool: {
      min: number;
      max: number;
    };
  };
}
 
type PartialConfig = DeepPartial<AppConfig>;
// 모든 중첩 프로퍼티가 선택적으로 변환됨
// 부분 설정으로 기본값을 덮어쓸 때 유용
 
function mergeConfig(
  defaults: AppConfig,
  overrides: DeepPartial<AppConfig>
): AppConfig {
  // 깊은 병합 로직...
  return { ...defaults, ...overrides } as AppConfig;
}

Builder 패턴 타입

타입 안전 Builder
typescript
type Builder<T, Built extends Partial<T> = {}> = {
  [K in keyof Omit<T, keyof Built> as `set${Capitalize<string & K>}`]: (
    value: T[K]
  ) => Builder<T, Built & Pick<T, K>>;
} & (keyof Omit<T, keyof Built> extends never
  ? { build: () => T }
  : {});
 
// 모든 필수 프로퍼티가 설정되어야만 build() 메서드가 노출됨
Info

Builder 패턴에서 매핑 타입은 "아직 설정되지 않은 프로퍼티"에 대한 setter만 노출하고, 모든 프로퍼티가 설정되면 build() 메서드를 노출하는 정교한 타입 설계를 가능하게 합니다.

매핑 타입의 성능 고려사항

매핑 타입은 강력하지만, 복잡한 중첩이나 큰 유니온에 적용하면 컴파일 성능에 영향을 줄 수 있습니다.

typescript
// 주의: 큰 유니온에 대한 매핑은 성능 저하를 유발할 수 있음
type HugeUnion = "a" | "b" | "c" | /* ... 수백 개 */ "z";
 
// 이런 경우 타입 인스턴스화가 조합적으로 폭발할 수 있음
type Problematic = {
  [K in HugeUnion]: SomeComplexType<K>;
};
Tip

매핑 타입의 성능을 개선하려면 불필요한 중첩을 줄이고, 가능하면 TypeScript 내장 유틸리티 타입(Partial, Required, Pick, Omit)을 사용하세요. 내장 타입은 컴파일러에서 특별히 최적화되어 있습니다.

정리

매핑 타입은 기존 타입을 기반으로 새로운 타입을 생성하는 TypeScript의 핵심 메커니즘입니다. 수정자 조작(readonly, ? 추가/제거), 키 재매핑(as 절), 조건부 타입과의 결합을 통해 CRUD 타입 생성, 이벤트 시스템, Builder 패턴 등 복잡한 타입 변환을 선언적으로 표현할 수 있습니다.

다음 장에서는 TypeScript 타입 시스템의 또 다른 강력한 도구인 템플릿 리터럴 타입을 다룹니다. 문자열 패턴 매칭, 문자열 조작 유틸리티, 그리고 매핑 타입과의 결합 패턴을 배웁니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#typescript#performance#devtools

관련 글

프로그래밍

7장: 템플릿 리터럴 타입으로 문자열 다루기

TypeScript 템플릿 리터럴 타입의 원리, 내장 문자열 조작 유틸리티, 패턴 매칭, 그리고 매핑 타입과의 결합을 통한 실전 패턴을 다룹니다.

2026년 2월 3일·12분
프로그래밍

5장: 조건부 타입 심층 분석

TypeScript 조건부 타입의 원리, 분배적 조건부 타입, infer 키워드와의 조합, 그리고 실전 활용 패턴을 깊이 있게 다룹니다.

2026년 1월 30일·13분
프로그래밍

8장: infer 키워드와 타입 추론 마스터

TypeScript infer 키워드의 고급 활용 패턴, infer extends 구문, 공변/반변 위치에서의 추론, 그리고 실전 타입 추출 패턴을 심층 분석합니다.

2026년 2월 5일·14분
이전 글5장: 조건부 타입 심층 분석
다음 글7장: 템플릿 리터럴 타입으로 문자열 다루기

댓글

목차

약 12분 남음
  • 매핑 타입의 기본
  • 수정자 조작
  • 키 재매핑 (as 절)
    • 키 필터링
    • 키 변환 패턴
  • 조건부 타입과의 결합
    • Nullable 프로퍼티 추출
  • 실전 패턴
    • CRUD 타입 생성기
    • 타입 안전 이벤트 이미터
    • 깊은 Partial
    • Builder 패턴 타입
  • 매핑 타입의 성능 고려사항
  • 정리