Mapped Type은 기존 타입의 각 속성을 순회하면서 변환 해서 새 타입을 만드는 패턴입니다.

기본 문법

TYPESCRIPT
// { [K in keyof T]: 새로운 타입 }
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

이 문법은 for...in 루프처럼 동작합니다. KT의 각 키를 순회하면서 새 속성을 만듭니다.

TYPESCRIPT
interface User {
  name: string;
  age: number;
}

type ReadonlyUser = Readonly<User>;
// {
//   readonly name: string;
//   readonly age: number;
// }

type OptionalUser = Optional<User>;
// {
//   name?: string;
//   age?: number;
// }

수정자(Modifier) 추가와 제거

TYPESCRIPT
// + (추가) — 기본값이므로 생략 가능
type AddReadonly<T> = {
  +readonly [K in keyof T]: T[K]; // readonly 추가
};

// - (제거)
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]; // readonly 제거
};

type Required<T> = {
  [K in keyof T]-?: T[K]; // optional 제거
};
TYPESCRIPT
interface ReadonlyUser {
  readonly name: string;
  readonly age: number;
}

type MutableUser = Mutable<ReadonlyUser>;
// { name: string; age: number } — readonly가 제거됨

값 타입 변환

키는 그대로 두고 값 타입만 바꿀 수 있습니다.

TYPESCRIPT
// 모든 속성을 boolean으로 변환 — 폼 유효성 검사에 유용
type Flags<T> = {
  [K in keyof T]: boolean;
};

interface User {
  name: string;
  age: number;
  email: string;
}

type UserFlags = Flags<User>;
// { name: boolean; age: boolean; email: boolean }

// 모든 속성을 Promise로 래핑
type Async<T> = {
  [K in keyof T]: Promise<T[K]>;
};

type AsyncUser = Async<User>;
// { name: Promise<string>; age: Promise<number>; email: Promise<string> }

키 리매핑 (as 절)

TypeScript 4.1부터 as를 사용해서 키 이름을 변환할 수 있습니다.

TYPESCRIPT
// getter 함수를 생성하는 타입
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;
// }

키 필터링

as 절에서 never를 반환하면 해당 키를 제거할 수 있습니다.

TYPESCRIPT
// string 타입인 속성만 남기기
type OnlyStringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface User {
  name: string;
  age: number;
  email: string;
  isAdmin: boolean;
}

type StringUser = OnlyStringProps<User>;
// { name: string; email: string }

키 이름 변환

TYPESCRIPT
// 접두사 추가
type Prefixed<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K];
};

type OnUser = Prefixed<User, 'on'>;
// {
//   onName: string;
//   onAge: number;
//   onEmail: string;
//   onIsAdmin: boolean;
// }

유니온으로 순회

keyof가 아닌 유니온 타입으로도 순회할 수 있습니다.

TYPESCRIPT
type EventMap = {
  [K in 'click' | 'scroll' | 'keypress']: (event: Event) => void;
};
// {
//   click: (event: Event) => void;
//   scroll: (event: Event) => void;
//   keypress: (event: Event) => void;
// }

실전 패턴

폼 에러 메시지 타입

TYPESCRIPT
interface LoginForm {
  username: string;
  password: string;
  rememberMe: boolean;
}

// 각 필드에 에러 메시지를 매핑
type FormErrors<T> = {
  [K in keyof T]?: string; // 에러가 있으면 메시지, 없으면 undefined
};

const errors: FormErrors<LoginForm> = {
  username: '아이디를 입력해주세요',
  // password는 에러 없으므로 생략 가능
};

이벤트 핸들러 맵

TYPESCRIPT
interface Events {
  click: { x: number; y: number };
  keypress: { key: string };
  scroll: { offset: number };
}

type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (event: T[K]) => void;
};

type Handlers = EventHandlers<Events>;
// {
//   onClick: (event: { x: number; y: number }) => void;
//   onKeypress: (event: { key: string }) => void;
//   onScroll: (event: { offset: number }) => void;
// }

Partial, Required, Readonly는 전부 Mapped Type

면접에서 "Partial을 직접 구현해 보세요"라고 하면 Mapped Type으로 구현합니다.

TYPESCRIPT
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, V> = { [P in K]: V };

정리

  • Mapped Type은 [K in keyof T]로 타입의 각 속성을 순회하며 변환한다
  • +/-readonly, ? 수정자를 추가하거나 제거할 수 있다
  • as 절로 키 이름을 변환하거나 필터링(never)할 수 있다
  • Partial, Required, Readonly 등 내장 유틸리티 타입은 전부 Mapped Type이다
  • 폼 에러, 이벤트 핸들러 등 실전에서 매우 자주 사용되는 패턴이다
댓글 로딩 중...