Mapped Types — in keyof로 타입을 변환하는 패턴
Mapped Type은 기존 타입의 각 속성을 순회하면서 변환 해서 새 타입을 만드는 패턴입니다.
기본 문법
// { [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 루프처럼 동작합니다. K가 T의 각 키를 순회하면서 새 속성을 만듭니다.
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) 추가와 제거
// + (추가) — 기본값이므로 생략 가능
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 제거
};
interface ReadonlyUser {
readonly name: string;
readonly age: number;
}
type MutableUser = Mutable<ReadonlyUser>;
// { name: string; age: number } — readonly가 제거됨
값 타입 변환
키는 그대로 두고 값 타입만 바꿀 수 있습니다.
// 모든 속성을 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를 사용해서 키 이름을 변환할 수 있습니다.
// 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를 반환하면 해당 키를 제거할 수 있습니다.
// 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 }
키 이름 변환
// 접두사 추가
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가 아닌 유니온 타입으로도 순회할 수 있습니다.
type EventMap = {
[K in 'click' | 'scroll' | 'keypress']: (event: Event) => void;
};
// {
// click: (event: Event) => void;
// scroll: (event: Event) => void;
// keypress: (event: Event) => void;
// }
실전 패턴
폼 에러 메시지 타입
interface LoginForm {
username: string;
password: string;
rememberMe: boolean;
}
// 각 필드에 에러 메시지를 매핑
type FormErrors<T> = {
[K in keyof T]?: string; // 에러가 있으면 메시지, 없으면 undefined
};
const errors: FormErrors<LoginForm> = {
username: '아이디를 입력해주세요',
// password는 에러 없으므로 생략 가능
};
이벤트 핸들러 맵
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으로 구현합니다.
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이다
- 폼 에러, 이벤트 핸들러 등 실전에서 매우 자주 사용되는 패턴이다
댓글 로딩 중...