매핑 타입 — keyof 와 [K in T]
한 타입의 모든 키에 변형을 적용하는 가장 강력한 도구.
11편(유틸리티) 의 Partial·Readonly·Pick 같은 도구는 모두 매핑 타입으로 만들어집니다. 매핑 타입은 "한 타입의 모든 키를 돌면서 새로운 모양으로" — 객체 타입에서 새 객체 타입을 만드는 메타 도구. 13편은 그 핵심 문법과 modifier·as 리매핑까지.
keyof — 키들의 유니온
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User;
// = "id" | "name" | "email"
// 인덱스 접근 — 값 타입 뽑기
type UserName = User["name"]; // string
type UserId = User["id"]; // number
type AnyValue = User[keyof User]; // string | number (모든 값 타입 유니온)
매핑 타입 — [K in T]: V
// Partial 의 실제 정의
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// ^ T 의 모든 키를 돌면서 옵션(?) 으로 만들고, 값 타입은 유지
// Readonly 의 정의
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Pick 의 정의
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
읽는 방법. "K 가 keyof T 의 각 값을 차례로 가지면서, 새 객체의 K 키에 T[K] 타입 부여". 함수형 언어의 map 과 같은 형태 — 입력 타입의 각 속성을 변형해 새 타입을 빚어냅니다.
modifier — readonly·? 추가/제거
// 추가
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
// 제거 (앞에 - 붙이면 옵션/readonly 제거)
type Required<T> = { [K in keyof T]-?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// 활용 — "선언은 readonly 였지만 잠시 풀기"
type WritableUser = Mutable<Readonly<User>>;
as 리매핑 — 키 이름까지 바꾸기 (TS 4.1+)
// "각 키에 getX 메서드 만들기"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
// 특정 키 제외 (Omit 직접 구현)
type RemoveByName<T, Names> = {
[K in keyof T as Exclude<K, Names>]: T[K];
};
as 절은 키 이름을 매번 새로 계산할 수 있게 해줍니다. 템플릿 리터럴 타입(`get${...}`) 과 조합하면 진짜 강력해집니다.
실전 — Form 상태 자동 만들기
interface UserForm {
name: string;
age: number;
email: string;
}
// 각 필드의 검증 결과
type FieldErrors<T> = {
[K in keyof T]?: string;
};
type UserFormErrors = FieldErrors<UserForm>;
// { name?: string; age?: string; email?: string }
// 각 필드의 dirty 플래그
type DirtyFlags<T> = {
[K in keyof T]: boolean;
};
type UserFormDirty = DirtyFlags<UserForm>;
// { name: boolean; age: boolean; email: boolean }
"함수만 골라내기" 패턴
// 메서드(함수) 키만 남기기 — as 로 필터링
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface Service {
id: number;
name: string;
start(): void;
stop(): void;
}
type ServiceMethods = FunctionKeys<Service>;
// = "start" | "stop"
type MethodsOnly = Pick<Service, FunctionKeys<Service>>;
// { start(): void; stop(): void }
가장 많이 쓰는 트릭. 매핑 타입 + 조건부 타입 + 인덱스 접근([keyof T]) 조합으로 "조건에 맞는 키만" 얻기. {[K in keyof T]: 조건 ? K : never}[keyof T] 가 황금 패턴 — 12편의 조건부와 함께 거의 모든 라이브러리 타입의 뼈대입니다.
Record 의 실체
// Record 도 매핑 타입
type MyRecord<K extends string | number | symbol, V> = {
[P in K]: V;
};
type PermsByRole = MyRecord<"admin" | "user", boolean>;
// { admin: boolean; user: boolean }
중첩 매핑 — 모든 깊이에 적용 (DeepReadonly 예제)
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
interface Config {
app: { name: string; version: string };
features: { auth: boolean };
}
const c: DeepReadonly<Config> = ...;
c.app.name = "x"; // ❌ readonly
c.features.auth = true; // ❌ readonly
유틸리티 vs 직접 만들기. 매핑 타입은 강력하지만 가독성이 빠르게 떨어집니다. 이름이 의미를 전달하지 못하면 좋은 추상화가 아님. DeepReadonly·Mutable 처럼 이름이 명확할 때만 분리, 한 번 쓸 거면 인라인.
한 표로 — 매핑 타입의 모든 변형
| 문법 | 의미 |
|---|---|
| [K in keyof T]: V | 모든 키 K 에 값 V |
| [K in keyof T]?: V | 옵션 추가 |
| [K in keyof T]-?: V | 옵션 제거 |
| readonly [K in keyof T] | readonly 추가 |
| -readonly [K in keyof T] | readonly 제거 |
| [K in keyof T as 새이름] | 키 리매핑 |
| [K in keyof T as never] | 키 제외 |
| {[K in keyof T]: ...}[keyof T] | 키 필터링 (값 추출) |
14편 — 타입 가드와 narrowing 깊이
typeof·instanceof·in·is·asserts 까지. unknown 을 안전하게 좁히기.
이전: 12편 조건부 타입 · 현재: 13편 (중급) · 다음 → 14편 타입 가드 · 진행: 13/20