본문으로 건너뛰기
김신건의 로그

[TypeScript] Mapped Types

· 수정 · 📖 약 1분 · 384자/단어 #typescript #types #mapped #advanced
TypeScript Mapped Types, TS Mapped Types, keyof mapping, key remapping, as clause, TS 매핑 타입

정의

Mapped Type 은 기존 타입의 각 필드를 순회 ([K in keyof T]) 하며 새 타입을 생성하는 문법입니다. readonly, ? modifier 를 붙이거나 제거 (-readonly, -?) 하고, as clause 로 키 이름을 재작명할 수 있습니다.

기본 구조

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

이는 T 를 그대로 복사. 여기에 변형을 추가:

type ReadOnly<T> = {
  readonly [K in keyof T]: T[K];
};

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

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type Stringified<T> = {
  [K in keyof T]: string;
};

Modifier 추가/제거

readonly / -readonly

type Immutable<T> = {readonly [K in keyof T]: T[K]};
type Mutable<T> = {-readonly [K in keyof T]: T[K]};

type A = Immutable<{x: number}>;   // {readonly x: number}
type B = Mutable<A>;                // {x: number}

? / -?

type Partial<T> = {[K in keyof T]?: T[K]};
type Required<T> = {[K in keyof T]-?: T[K]};

type A = Partial<{x: number}>;     // {x?: number}
type B = Required<A>;               // {x: number}

Key remapping (as)

TypeScript 4.1+ 부터 키 이름 재작명 가능.

type Prefixed<T> = {
  [K in keyof T as `get_${string & K}`]: () => T[K];
};

type User = {id: number; name: string};
type UserGetters = Prefixed<User>;
// = {get_id: () => number; get_name: () => string}

조건부 키 필터

never 반환 시 필드 제거:

type OmitNever<T> = {
  [K in keyof T as T[K] extends never ? never : K]: T[K];
};

type A = {x: number; y: never; z: string};
type B = OmitNever<A>;   // {x: number; z: string}

필드 이름으로 필터

type OnlyEndingWithId<T> = {
  [K in keyof T as K extends `${string}Id` ? K : never]: T[K];
};

type A = {name: string; userId: number; postId: number; active: boolean};
type B = OnlyEndingWithId<A>;   // {userId: number; postId: number}

값 타입으로 필터

type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

type A = {id: number; name: string; age: number; active: boolean};
type B = PickByValue<A, number>;   // {id: number; age: number}

as const 와 결합

const CONFIG = {host: "localhost", port: 8080} as const;
type Config = typeof CONFIG;       // {readonly host: "localhost"; readonly port: 8080}
type Keys = keyof Config;           // "host" | "port"
type Values = Config[Keys];         // "localhost" | 8080

여러 필드에서 유래한 타입

type MakeGetterSetter<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
} & {
  [K in keyof T as `set${Capitalize<string & K>}`]: (v: T[K]) => void;
};

type User = {name: string; age: number};
type UserAccessors = MakeGetterSetter<User>;
// = {getName: () => string; getAge: () => number;
//    setName: (v: string) => void; setAge: (v: number) => void}

Deep transformation

DeepReadonly

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

type A = {user: {name: string; age: number}};
type B = DeepReadonly<A>;
// = {readonly user: {readonly name: string; readonly age: number}}

DeepPartial

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

DeepReplace type

type Replace<T, From, To> = {
  [K in keyof T]: T[K] extends From ? To :
    T[K] extends object ? Replace<T[K], From, To> :
    T[K];
};

type A = {a: number; b: {c: number; d: string}};
type B = Replace<A, number, string>;
// = {a: string; b: {c: string; d: string}}

Union 을 union of key 로

type ToKeyValueUnion<T> = {
  [K in keyof T]: {key: K; value: T[K]};
}[keyof T];

type User = {id: number; name: string; email: string};
type Entry = ToKeyValueUnion<User>;
// = {key: "id"; value: number}
//   | {key: "name"; value: string}
//   | {key: "email"; value: string}

[keyof T] 인덱싱으로 union 화.

Enum 뒤집기

type Reverse<T> = {
  [K in keyof T as T[K] extends string | number ? T[K] : never]: K;
};

const COLORS = {RED: "red", GREEN: "green", BLUE: "blue"} as const;
type Colors = typeof COLORS;
type Reversed = Reverse<Colors>;
// = {red: "RED"; green: "GREEN"; blue: "BLUE"}

Tuple 매핑

type MapTuple<T extends any[], U> = {
  [K in keyof T]: U;
};

type A = MapTuple<[1, 2, 3], string>;   // [string, string, string]

type NullableTuple<T extends any[]> = {
  [K in keyof T]: T[K] | null;
};

type B = NullableTuple<[string, number]>;   // [string | null, number | null]

Tuple 도 keyof 대상. keyof [1, 2, 3]"0" | "1" | "2" | "length" | ... 를 포함.

실전: form 스키마

type FormSchema<T> = {
  [K in keyof T]: {
    label: string;
    type: T[K] extends boolean ? "checkbox" :
          T[K] extends number ? "number" :
          T[K] extends Date ? "date" :
          "text";
    required: T[K] extends undefined ? false : true;
    default?: T[K];
  };
};

type User = {name: string; age: number; admin?: boolean; birth: Date};
type UserForm = FormSchema<User>;

실전: SQL row -> object

type SnakeToCamel<S extends string> =
  S extends `${infer H}_${infer R}` ? `${H}${Capitalize<SnakeToCamel<R>>}` : S;

type CamelKeys<T> = {
  [K in keyof T as SnakeToCamel<string & K>]: T[K];
};

type Row = {user_id: number; created_at: Date; is_active: boolean};
type Camel = CamelKeys<Row>;
// = {userId: number; createdAt: Date; isActive: boolean}

함정

WARNING

[K in keyof T] 는 keyof T 를 매번 재계산. 성능 이슈 시 alias 로 저장.

CAUTION

as never 로 필드 제거는 조건적. Non-conditional 에서는 무의미.

WARNING

DeepReadonly 는 primitive 를 만나면 정지. Array/Function 처리에 주의 (Array 는 object 로 매칭됨 -> 원치 않은 결과 가능).

IMPORTANT

keyof T 는 string | number | symbol. 문자열 조작 (template literal) 시 string & K 로 좁혀야 함.

CAUTION

재귀 depth. DeepReadonly 등 재귀는 TypeScript 제한 (50 단계) 에 걸릴 수 있음.

관련 위키

이 글의 용어 (8개)
[TypeScript] Conditional Typestypescript
정의 Conditional Type 은 형태로 타입 레벨에서 조건 분기를 표현합니다. 키워드로 매칭된 타입을 추출하고, union 에 대해서는 자동 분배 (distributive…
[TypeScript] Genericstypescript
정의 Generics 는 타입을 파라미터화하는 문법입니다. 함수, 클래스, 인터페이스, type alias 가 재사용 가능 하면서도 타입 안전 하도록 만듭니다. Java 의 ge…
[TypeScript] Interfacestypescript
정의 Interface 는 객체의 shape (형태) 을 정의하는 TypeScript 문법입니다. 필드, 메서드, 인덱스 시그니처, 호출 시그니처를 선언하며, 로 상속하고 dec…
[TypeScript] Template Literal Typestypescript
정의 Template Literal Type 은 TypeScript 4.1 에서 도입된 문법으로, JavaScript template literal 처럼 백틱과 를 이용해 타입 …
[TypeScript] Type Aliasestypescript
정의 Type Alias 는 타입에 이름을 붙이는 TypeScript 문법 ( ). Interface 는 객체 shape 만 표현하지만, type alias 는 모든 타입 표현식…
[TypeScript] Type Narrowing (Type Guards)typescript
정의 Type Narrowing 은 TypeScript 컴파일러가 제어 흐름 분석 (control flow analysis) 을 통해 특정 위치에서 값의 타입을 더 좁게 추론하는…
[TypeScript] Utility Typestypescript
정의 Utility Types 는 TypeScript 표준 라이브러리에 포함된 미리 정의된 generic 타입 들입니다. 기존 타입을 변형/구성하는 흔한 패턴을 제공하며, map…
TypeScripttypescript
정의 TypeScript 는 Microsoft 가 2012년 발표한 JavaScript 의 상위 집합 프로그래밍 언어입니다. 정적 타입 시스템, 인터페이스, 제네릭, enum, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기