Menu

TypeScript 매핑된 타입(Mapped Types): 문법, 제한자, 키 재매핑

매핑된 타입은 키를 순회하며 새 객체 타입을 만듭니다: { [K in keyof T]: ... }. 문법, +와 -를 쓰는 readonly와 ? 제한자, as로 하는 키 재매핑, 키 걸러 내기, 그리고 Partial, Readonly, Required, Pick, Record가 작성된 방식을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

매핑된 타입은 키 집합을 순회하며 새 객체 타입을 만듭니다. { [K in keyof T]: boolean }은 "T의 모든 키 K에 대해, boolean 타입의 K라는 속성"이라는 뜻입니다.

Features에 키를 추가하면 Flags<Features>도 그것을 요구합니다. 매핑된 타입은 컴파일 시점에만 존재합니다. 객체를 기술할 뿐, 만들지는 않습니다.

문법

매핑된 타입에는 세 부분이 있습니다. 키 변수, 순회할 키의 유니언, 그리고 키 변수를 쓸 수 있는 속성 타입입니다.

type MappedType = {
  [K in Keys]: PropertyType; // K takes each member of Keys in turn
};

Keys는 문자열, 숫자, 심볼의 어떤 유니언이든 될 수 있습니다. keyof에서 올 필요는 없습니다.

이것이 정확히 Record<Size, number>가 하는 일입니다. Record는 한 줄짜리 매핑된 타입입니다. 속성 타입은 키를 쓸 수 있습니다. { [K in keyof T]: T[K] }는 인덱스 접근을 통해 각 속성 타입을 복사하고, { [K in keyof T]: T[K] | null }은 모든 속성을 null 가능하게 만듭니다.

제한자: +와 -를 쓰는 readonly와 ?

매핑된 타입은 모든 속성의 readonly와 선택적(?) 제한자를 한꺼번에 더하거나 뺄 수 있습니다. 앞에 +를 붙이면 더하고(아무것도 쓰지 않을 때의 기본값), -를 붙이면 뺍니다.

마지막 줄은 readonly가 컴파일 시점의 규칙일 뿐이라는 것을 보여 줍니다. 대입은 보고되었지만(여기서는 @ts-expect-error로 억제), 출력된 JavaScript는 그대로 실행했습니다. -?는 물음표를 빼는 것 이상을 합니다. 속성 타입에서 undefined도 없애므로, AllRequired<Account>는 { id: 1, email: undefined }를 error TS2322: Type 'undefined' is not assignable to type 'string'으로 거부합니다.

Partial, Readonly, Pick, Record가 만들어진 방식

객체의 모양을 바꾸는 내장 유틸리티 타입은 매핑된 타입입니다. TypeScript 자체의 lib.es5.d.ts에 있는 정의입니다.

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

이것을 읽어 보는 것은 좋은 연습입니다. Pick은 넘긴 키만 순회하고, Record는 원본 타입을 무시하고 모든 키에 같은 값 타입을 줍니다. Omit은 자체적인 매핑된 타입이 아니라 Pick<T, Exclude<keyof T, K>>입니다. 유틸리티 타입 레퍼런스에 모두 정리되어 있습니다.

as로 키 재매핑하기

키 뒤의 as 절은 속성 이름을 바꿉니다. 템플릿 리터럴 타입과 조합하면 기존 이름에서 새 이름을 만들 수 있습니다.

keyof T에는 number와 symbol 키가 포함될 수 있고 Capitalize는 문자열만 받으므로 string & K가 있습니다. 교차 타입이 문자열 키만 남기고 나머지를 버립니다.

never로 키 걸러 내기

as 절이 어떤 키에 대해 never를 만들면 그 키는 제거됩니다. 조건부 타입과 함께 쓰면 속성의 타입에 따라 남기거나 버릴 수 있습니다.

DataOnly 같은 타입은 JSON.stringify를 거친 뒤 남는 것이나 폼이 편집하는 것, 즉 메서드를 뺀 필드를 기술할 때 유용합니다.

유지되는 제한자

어떤 타입 T의 keyof T를 순회하는 매핑된 타입을 준동형(homomorphic)이라고 하며, T에서 각 속성의 readonly와 ? 제한자를 복사합니다. 평범한 키 유니언을 순회하는 매핑된 타입은 제한자 없이 시작합니다.

type Account = { readonly id: number; email?: string };

type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string }  (modifiers kept)

type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined }  (email is required now)

Account["email"]에 undefined가 포함되므로 B의 email 타입에는 여전히 undefined가 있지만, 속성 자체는 더 이상 선택적이지 않습니다. { id: 1 }은 거부됩니다. 이것이 Partial<T> 같은 타입이 keyof T를 순회하는 이유입니다. 명시적으로 바꾸지 않는 것은 모두 유지합니다.

제네릭 준동형 매핑된 타입을 배열이나 튜플에 적용하면, 숫자 키를 가진 객체가 아니라 배열이나 튜플이 만들어집니다. Readonly<string[]>는 readonly string[]이고, type Box<T> = { [K in keyof T]: { value: T[K] } }라면 Box<[string, number]>는 [{ value: string }, { value: number }]입니다.

매핑된 타입과 런타임 코드

매핑된 타입은 객체를 기술할 뿐이므로, 그런 객체를 만드는 함수에는 자체 런타임 반복문이 필요합니다. 그러면 타입이 그 결과를 기술합니다.

콜백은 아무 표기 없이 매핑된 타입에서 매개변수 타입을 얻습니다(한쪽의 v는 string, 다른 쪽은 number). 타입 매핑이 아니라 Map 클래스를 찾아 여기 왔다면 TypeScript의 Map을 보세요.

자주 묻는 질문

TypeScript에서 매핑된 타입이란 무엇인가요?

키의 유니언을 순회해서 객체 타입을 만드는 타입입니다: { [K in Keys]: SomeType }. 대개 키는 keyof T이므로, 새 타입은 T와 같은 키에 변환된 속성 타입을 갖습니다. 예: type Flags<T> = { [K in keyof T]: boolean }.

매핑된 타입에서 +readonly, -readonly, +?, -?는 무슨 뜻인가요?

제한자를 더하거나 뺍니다. readonly나 +readonly는 모든 속성을 readonly로 만들고, -readonly는 readonly를 없앱니다. ?나 +?는 모든 속성을 선택적으로 만들고, -?는 필수로 만들면서 타입에서 undefined도 없앱니다. Required<T>는 -?로 작성되어 있습니다.

매핑된 타입에서 키 이름을 바꾸려면 어떻게 하나요?

키 뒤에 as 절을 쓰세요: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }는 name을 getName으로 바꿉니다. as 절에서 키를 never로 매핑하면 그 키가 제거되며, 이것이 속성을 걸러 내는 방법입니다.

매핑된 타입은 TypeScript의 Map과 같은 것인가요?

아니요. 매핑된 타입은 컴파일 시점의 타입 변환이고 코드를 만들지 않습니다. Map<K, V>는 JavaScript의 Map 클래스로, 런타임의 키/값 쌍 컬렉션입니다. "typescript map type"을 검색하는 사람은 둘 중 하나를 뜻하는 경우가 많습니다.

Partial과 Readonly는 어떻게 구현되어 있나요?

표준 라이브러리에 매핑된 타입으로 있습니다: type Partial<T> = { [P in keyof T]?: T[P] }, type Readonly<T> = { readonly [P in keyof T]: T[P] }. Pick과 Record도 매핑된 타입이고, Omit은 Pick과 Exclude의 조합입니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기