타입스크립트 교재 · 13편 / 20편

매핑 타입 — keyof 와 [K in T]

한 타입의 모든 키에 변형을 적용하는 가장 강력한 도구.

중급읽는 시간 7분2026-05-17
기존 타입의 키들을 변형해 새 타입을 만드는 매핑 도식

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

© 2026 주나이테크(주) @JUNAITECH