본문으로 건너뛰기

안동민 개발노트

본문 시작

매핑된 타입

keyof로 기존 타입의 키를 순회해 속성 타입과 선택·읽기 전용 수정자를 변환하고 키를 다시 매핑합니다.

타입스크립트의 또 다른 강력한 고급 기능이 매핑된 타입(Mapped Types)입니다.

매핑된 타입은 기존 타입의 속성을 순회하면서 새로운 타입으로 변환하는 방식입니다.

자바스크립트의 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 }; // 유효함

매핑 수정자는 원본 객체 타입을 복사하면서 속성의 선택성이나 읽기 전용 여부만 바꿀 때 사용합니다.

아래 다이어그램은 각 기호가 어떤 속성 규칙을 더하거나 제거하는지 압축해서 보여줍니다.

매핑 수정자 효과표

매핑된 타입은 키를 순회하면서 선택성, 읽기 전용 여부, 값 타입을 독립적으로 조정할 수 있습니다.

  1. 선택 속성 추가

    ? Partial<T> 처럼 모든 필드를 있어도 되고 없어도 되는 형태로 만듭니다.

  2. 선택 속성 제거

    -? Required<T> 처럼 누락을 허용하지 않는 계약으로 바꿉니다.

  3. 변경 차단

    readonly Readonly<T> 처럼 속성 재할당을 막는 타입을 만듭니다.

  4. 변경 허용

    -readonly 읽기 전용으로 받은 타입을 내부 수정 가능한 복사본으로 바꿀 때 사용합니다.

interface Config {
  readonly id: string;
  host?: string;
  port?: number;
}
type MutableRequired<T> = {
  -readonly [P in keyof T]-?: T[P];
};

키 다시 매핑

Mapped Type은 키를 순회하고 필요하면 이름까지 다시 만든다

keyof로 키 집합을 만들고, 각 키의 value 타입을 읽은 뒤 as 절로 key 이름을 바꿀 수 있다.

  1. keyof T
    keyof T 원본 객체의 key

    union 생성

  2. [K in keyof T]
    [K in keyof T] 각 key

    하나씩 순회

  3. T[K]
    T[K] 해당 key의 value 타입

    보존

  4. as NewKey
    as NewKey 템플릿 리터럴

    key 이름 재작성

기준해석
readonly/?modifier를 추가하거나 제거 가능
Pick/Omitmapped type과 key filtering의 대표 사례
판단객체 shape 변환은 key 순회로 읽는다

타입스크립트 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
키 이름을 바꿔도 값 타입은 따라옵니다

as 절은 속성 이름을 새로 만들고, T[P] 는 원래 필드의 값 타입을 콜백 인자로 이어 줍니다.

  1. source
    OnChangeCallbacks가 만들어지는 레일

    source firstName, lastName, email 속성을 가진 입력 타입입니다. 반복 각 키를 하나씩 꺼내 새 속성 이름의 재료로 사용합니다. rename 템플릿 리터럴과 Capitalize로 콜백 이름을 만듭니다. value 원래 필드가 string이면 콜백 인자도 string으로 유지됩니다. optional 필요한 핸들러만 구현할 수 있도록 선택 속성으로 둡니다.

  2. source
    FormFields

    source firstName, lastName, email 속성을 가진 입력 타입입니다.

  3. 반복
    P in keyof T

    반복 각 키를 하나씩 꺼내 새 속성 이름의 재료로 사용합니다.

  4. rename
    as onChange 매핑

    rename 템플릿 리터럴과 Capitalize로 콜백 이름을 만듭니다.

  5. value
    value: T[P]

    value 원래 필드가 string이면 콜백 인자도 string으로 유지됩니다.

  6. optional
    ?: callback

    optional 필요한 핸들러만 구현할 수 있도록 선택 속성으로 둡니다.


내장 매핑된 유틸리티 타입

타입스크립트는 매핑된 타입을 기반으로 미리 정의된 여러 유용한 유틸리티 타입들(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 타입인 객체 타입을 만듭니다.

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

매핑된 타입을 설계할 때는 키 집합, 속성 수정자, 키 이름 변환, 값 타입 변환을 순서대로 나누면 복잡도를 낮출 수 있습니다.

매핑된 타입 변환 파이프라인

객체 타입 변환은 키 집합을 순회하면서 속성 규칙과 값 타입을 단계적으로 바꾸는 작업입니다.

  1. keyof T
    키 수집

    원본 객체의 속성 이름을 유니온으로 만든 뒤 순회합니다.

  2. +?
    선택성 조정

    필수와 선택 속성을 `?`, `-?`로 목적에 맞게 바꿉니다.

  3. as
    키 이름 변환

    템플릿 리터럴 타입으로 이벤트 이름이나 API 필드를 만듭니다.

  4. T[P]
    값 타입 변환

    원본 속성 타입을 읽어 래핑, 제거, 변환 규칙을 적용합니다.

  5. 5
    형태 복제

    전체 구조를 유지하면 `Partial`, `Readonly` 계열이 맞습니다.


매핑된 타입은 기존 객체 타입의 속성을 순회하며 새로운 타입을 만드는 기능입니다.

readonly? 수정자, 조건부 타입, 키 다시 매핑을 조합하면 API 응답, 폼 상태, 업데이트 DTO처럼 반복되는 타입 변환을 한 곳에서 표현할 수 있습니다.


다음 다이어그램은 매핑된 타입을 키 순회, 수정자, 키 다시 매핑 기준으로 정리한 표입니다.

매핑된 타입은 객체 키를 순회해 새 타입을 만듭니다

keyof로 키 집합을 얻고 in으로 순회하며 readonly, optional 수정자와 key remapping으로 객체 타입을 변환합니다.

  1. 키 집합

    keyof 객체 타입의 속성 이름을 union으로 꺼내 매핑 대상 목록을 만듭니다. keyof User

  2. 속성 순회

    in 매핑 각 키를 돌며 값 타입을 유지하거나 새 규칙으로 변환합니다. [K in keyof T]

  3. 속성 정책

    수정자 readonly와 optional 표시를 붙이거나 제거해 변형 타입을 만듭니다. -readonly

  4. 이름 변환

    키 재매핑 as 절과 template literal type을 이용해 새 속성 이름을 계산합니다. as `get${K}`

매핑된 타입은 속성 목록을 순회해 새 구조를 만듭니다

keyof로 얻은 키 집합을 따라가며 readonly, optional, 키 이름 변경 같은 변환을 타입 수준에서 반복합니다.

  1. 속성별 변환

    키 순회 [K in keyof T] 형태로 기존 타입의 모든 속성을 한 번씩 처리합니다. keyof T

  2. 읽기와 선택 제어

    수정자 readonly와 ?를 붙이거나 제거해 불변 객체나 부분 객체 타입을 만듭니다. -readonly

  3. 이름 변환

    키 재매핑 as 절을 사용해 기존 속성 이름을 새로운 규칙의 이름으로 바꿀 수 있습니다. as NewKey

  4. 표준 변환 사용

    내장 유틸 Partial, Required, Readonly, Pick 같은 유틸리티가 매핑된 타입 위에서 동작합니다. Partial<T>

아래 다이어그램은 매핑된 타입이 객체 속성을 순회하며 새 타입을 만드는 과정을 정리합니다.

매핑된 타입 변환표

매핑된 타입은 기존 객체 타입의 키를 순회하며 readonly, optional, 값 타입을 일괄 변환합니다.

  1. 키 순회
    keyof와 in

    키 순회 객체 타입의 속성 이름들을 유니온으로 꺼내 하나씩 새 속성으로 만듭니다. [P in keyof T]

  2. 수정자
    readonly와 optional 조절

    수정자 +readonly, -readonly, -? 같은 표기로 속성 한정자를 바꿉니다. -readonly [P in K]

  3. 키 다시 매핑
    as로 이름 변경

    키 다시 매핑 템플릿 리터럴 타입과 함께 속성 이름을 새 규칙으로 바꿀 수 있습니다. as `get${Capitalize<K>}`

  4. 내장 유틸리티
    Partial과 Readonly

    내장 유틸리티 자주 쓰는 객체 변환은 표준 유틸리티 타입으로 이미 제공됩니다. Partial<T>

아래 다이어그램은 매핑된 타입을 실제 코드에 적용하기 전에 설정 위치, 타입 해석, 빌드 결과를 확인합니다.