icon
5장 : 고급 타입

매핑된 타입


타입스크립트의 또 다른 강력한 고급 기능은 바로 매핑된 타입(Mapped Types) 입니다. 매핑된 타입은 기존 타입의 각 속성을 순회(iterate)하면서 새로운 타입으로 변환하는 방식입니다. 마치 자바스크립트의 map 함수가 배열의 각 요소를 변환하여 새로운 배열을 만들듯이, 매핑된 타입은 객체 타입의 각 속성을 변환하여 새로운 객체 타입을 생성합니다.

이 기능을 통해 우리는 기존 타입을 기반으로 다양한 변형된 타입을 손쉽게 만들 수 있으며, 이는 타입 정의의 중복을 줄이고 코드의 유연성을 크게 높여줍니다.


매핑된 타입의 기본 문법

매핑된 타입의 기본 문법은 다음과 같습니다.

type NewType<T> = {
  [P in K]: SomeType;
};
  • T: 변환의 대상이 되는 기존 타입 (주로 객체 타입)
  • P: 기존 타입 T의 속성 이름 하나하나를 나타내는 타입 변수
  • in: for...in 루프처럼 K에 있는 속성들을 순회하겠다는 의미
  • K: 순회할 속성 이름들의 유니온 타입 (주로 keyof T를 사용)
  • SomeType: P에 해당하는 새로운 속성에 부여할 타입

예시를 통해 살펴보겠습니다. 기존 객체 타입의 모든 속성을 readonly로 만드는 Readonly<T> 유틸리티 타입을 직접 구현해 봅시다.

interface User {
  id: number;
  name: string;
  email?: string;
}

// ReadonlyType은 User 타입의 모든 속성을 읽기 전용으로 만듭니다.
type ReadonlyType<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadonlyUser = ReadonlyType<User>;
/*
// ReadonlyUser의 실제 타입은 다음과 같습니다.
type ReadonlyUser = {
  readonly id: number;
  readonly name: string;
  readonly email?: string;
};
*/

const user: ReadonlyUser = {
  id: 1,
  name: "김타입",
  email: "kim@example.com",
};

// user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
user.name; // 접근은 가능

이 예시에서 [P in keyof T]T 타입의 모든 속성 이름(id, name, email)을 P에 할당하며 순회합니다. 그리고 각 Preadonly 한정자를 붙이고, 원래 T 타입의 P 속성 타입(T[P])을 그대로 사용합니다.


매핑 수정자

매핑된 타입을 사용할 때, 기존 속성의 readonly 또는 ? (선택적) 한정자를 추가하거나 제거할 수 있습니다. 이를 매핑 수정자(Mapping Modifiers) 라고 합니다.

  • + 또는 -: 한정자를 추가하거나 제거합니다.
    • +readonly: readonly 한정자를 추가
    • -readonly: readonly 한정자를 제거
    • +?: ? (선택적) 한정자를 추가
    • -?: ? (선택적) 한정자를 제거

+- 기호는 생략될 수 있으며, 생략되면 기본적으로 +와 동일하게 동작합니다. 예를 들어, readonly [P in keyof T]+readonly [P in keyof T]와 같습니다.

예시를 통해 각각의 사용법을 살펴보겠습니다.

모든 속성을 선택적으로 만들기 (Partial<T> 구현)

type PartialType<T> = {
  [P in keyof T]?: T[P]; // '?'를 붙여 모든 속성을 선택적으로 만듭니다.
};

interface Product {
  id: number;
  name: string;
  price: number;
}

type OptionalProduct = PartialType<Product>;
/*
type OptionalProduct = {
  id?: number;
  name?: string;
  price?: number;
};
*/
const p1: OptionalProduct = { name: "키보드" }; // 유효함
const p2: OptionalProduct = {};               // 유효함

모든 속성을 필수로 만들기 (Required<T> 구현)

type RequiredType<T> = {
  [P in keyof T]-?: T[P]; // '-'와 '?'를 붙여 모든 속성을 필수로 만듭니다.
};

interface Config {
  port?: number;
  host?: string;
  debug?: boolean;
}

type StrictConfig = RequiredType<Config>;
/*
type StrictConfig = {
  port: number;
  host: string;
  debug: boolean;
};
*/

// const c1: StrictConfig = { port: 8080 }; // Error: 'host' 및 'debug' 속성이 누락됨
const c2: StrictConfig = { port: 3000, host: "localhost", debug: true }; // 유효함

키 다시 매핑

타입스크립트 4.1부터는 매핑된 타입의 키를 변환할 수 있는 키 다시 매핑(Key Remapping) 기능이 추가되었습니다. 이는 as 키워드를 사용하여 새로운 키 이름을 정의함으로써 가능합니다.

type MappedTypeWithNewKeys<T> = {
  [P in keyof T as NewKeyType]: T[P];
};
  • NewKeyType: P와 다른 타입 유틸리티를 사용하여 새롭게 생성될 키의 타입.

예시: 객체의 모든 키 앞에 get을 붙여 Getter 속성처럼 만들기

interface UserProfile {
  name: string;
  age: number;
  isActive: boolean;
}

// Getters라는 새로운 타입을 정의합니다.
// 모든 속성 키 앞에 'get'을 붙이고 첫 글자를 대문자로 변환합니다.
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: T[P];
};

type UserProfileGetters = Getters<UserProfile>;
/*
// UserProfileGetters의 실제 타입은 다음과 같습니다.
type UserProfileGetters = {
  getName: string;
  getAge: number;
  getIsActive: boolean;
};
*/

// 예시 사용:
let userGetters: UserProfileGetters = {
  getName: "Alice",
  getAge: 30,
  getIsActive: true,
};

console.log(userGetters.getName); // Alice

여기서 Capitalize<string & P>P (원래 속성 이름)의 첫 글자를 대문자로 변환하는 내장 유틸리티 타입입니다. string & PP가 반드시 string 타입임을 보증합니다.

또 다른 예시: 모든 속성을 onChange 접두사와 함께 콜백 함수 타입으로 변환하기

interface FormFields {
  firstName: string;
  lastName: string;
  email: string;
}

type OnChangeCallbacks<T> = {
  [P in keyof T as `onChange${Capitalize<string & P>}`]?: (value: T[P]) => void;
};

type UserFormCallbacks = OnChangeCallbacks<FormFields>;
/*
// UserFormCallbacks의 실제 타입은 다음과 같습니다.
type UserFormCallbacks = {
  onChangeFirstName?: (value: string) => void;
  onChangeLastName?: (value: string) => void;
  onChangeEmail?: (value: string) => void;
};
*/

const formHandler: UserFormCallbacks = {
  onChangeFirstName: (val) => console.log(`First name changed to: ${val}`),
  onChangeEmail: (val) => console.log(`Email changed to: ${val}`),
};

formHandler.onChangeFirstName?.("John"); // First name changed to: John
formHandler.onChangeEmail?.("john@example.com"); // Email changed to: john@example.com

내장 매핑된 유틸리티 타입

타입스크립트는 매핑된 타입을 기반으로 미리 정의된 여러 유용한 유틸리티 타입들(Built-in Mapped Utility Types)을 제공합니다.

  • Partial<T>: T의 모든 속성을 선택적으로 만듭니다. ([P in keyof T]?: T[P])
  • Required<T>: T의 모든 선택적 속성을 필수로 만듭니다. ([P in keyof T]-?: T[P])
  • Readonly<T>: T의 모든 속성을 읽기 전용으로 만듭니다. (readonly [P in keyof T]: T[P])
  • Pick<T, K>: T에서 K에 해당하는 속성들만 선택하여 새로운 타입을 만듭니다. ([P in K]: T[P])
  • Omit<T, K>: T에서 K에 해당하는 속성들을 제외한 새로운 타입을 만듭니다. (Pick<T, Exclude<keyof T, K>>와 유사)
  • Record<K, T>: K의 속성 이름을 가지며, 모든 속성 값이 T 타입인 객체 타입을 만듭니다.

이 유틸리티 타입들은 타입스크립트 개발에서 매우 자주 사용되므로, 그 사용법과 목적을 잘 이해하는 것이 중요합니다.


매핑된 타입은 기존 타입을 기반으로 새로운 타입을 생성하고 변형하는 데 있어 엄청난 유연성을 제공합니다. 이를 통해 코드 중복을 피하고, 복잡한 타입 변환 로직을 간결하게 표현할 수 있습니다. 매핑 수정자와 키 다시 매핑 기능을 활용하면 거의 모든 객체 타입 변환 시나리오를 커버할 수 있으며, 이는 타입스크립트 기반 프로젝트의 유지보수성과 확장성을 크게 향상시킵니다.