매핑 타입

매핑 타입 (Mapped Types)

보통 이 기법은 keyof 로 만든 키들의 유니온 타입을 순회하면서, 기존 타입의 모양을 그대로 두되 그 값만 바꾼 새 타입을 만들어 내요. DRY(반복하지 마세요) 원칙을 타입 영역에서 실현하는 방법이라고 생각하면 돼요.

출처: TypeScript 공식문서

본문

같은 코드를 반복하고 싶지 않을 때, 타입이 다른 타입을 기반으로 만들어져야 하는 경우가 있어요.

매핑 타입은 인덱스 시그니처(index signature) 문법 위에 세워져요. 인덱스 시그니처는 미리 선언하지 않은 속성들의 타입을 정해두는 문법인데, 이렇게 쓰죠.

type OnlyBoolsAndHorses = {
  [key: string]: boolean | Horse;
};

const conforms: OnlyBoolsAndHorses = {
  del: true,
  rodney: false,
};

매핑 타입은 PropertyKey 들의 유니온 타입(자주 keyof 로 만들어지죠)을 써서 키를 하나씩 순회하며 타입을 만들어 내는 제네릭 타입이에요. 여기서 PropertyKeystring | number | symbol을 가리키는 내장 별칭이에요. 자세한 내용은 인덱스 접근 타입 문서에서 다루고 있어요.

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

이 예시에서 OptionsFlags 는 타입 Type 의 모든 속성을 가져와서 그 값을 전부 boolean 으로 바꿔 줘요. 실제로 어떻게 보이는지 볼까요.

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
};

type FeatureOptions = OptionsFlags<Features>;
type FeatureOptions = {
    darkMode: boolean;
    newUserProfile: boolean;
}

매핑 수식어 (Mapping Modifiers)

매핑 중에 적용할 수 있는 수식어가 두 가지 더 있어요. 바로 readonly? 인데, 하나는 변경 가능 여부(mutability) 를, 하나는 선택 여부(optionality) 를 다뤄요. 이 수식어는 앞에 -+ 를 붙여서 없애거나 더할 수 있어요. 아무 접두어도 붙이지 않으면 + 가 가정돼요.

여기서는 - 를 써서 타입의 속성에서 readonly 를 떼어내는 예시를 볼게요.

// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};

type LockedAccount = {
  readonly id: string;
  readonly name: string;
};

type UnlockedAccount = CreateMutable<LockedAccount>;
type UnlockedAccount = {
    id: string;
    name: string;
}

이번에는 -? 를 써서 선택 속성을 전부 필수 속성으로 바꿔 보죠.

// Removes 'optional' attributes from a type's properties
type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};

type MaybeUser = {
  id: string;
  name?: string;
  age?: number;
};

type User = Concrete<MaybeUser>;
type User = {
    id: string;
    name: string;
    age: number;
}

as 를 통한 키 재매핑 (Key Remapping via as)

TypeScript 4.1 부터는 매핑 타입 안에서 as 절을 써서 키 자체를 다시 매핑할 수 있어요.

type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}

여기에 템플릿 리터럴 타입 같은 기능을 결합하면 기존 속성 이름으로부터 새 속성 이름을 만들어 낼 수 있어요. 아래처럼 namegetName 으로 만들 수 있죠.

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;
type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
}

조건부 타입(conditional type)으로 키에 never 를 만들어 주면 특정 키를 걸러 낼 수도 있어요. never가 된 키는 최종 타입에 남지 않거든요.

// Remove the 'kind' property
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
type KindlessCircle = {
    radius: number;
}

그리고 매핑 대상은 string | number | symbol 유니온뿐 아니라 아무 타입의 유니온이어도 돼요. 각 객체 타입의 kind 값을 키로 삼아, 그 이벤트에 맞는 핸들러로 매핑하는 예시를 볼게요.

type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}

type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };

type Config = EventConfig<SquareEvent | CircleEvent>
type Config = {
    square: (event: SquareEvent) => void;
    circle: (event: CircleEvent) => void;
}

더 알아보기 (Further Exploration)

매핑 타입은 이 타입 조작 섹션의 다른 기능들과도 잘 어울려요. 예를 들어, 어떤 객체에 pii 값이 리터럴 true 로 설정되어 있는지에 따라 true 또는 false를 돌려주는 매핑 타입과 조건부 타입의 조합이 있어요. 아래는 GDPR 삭제 대상 필드를 찾아내는 예시예요.

type ExtractPII<Type> = {
  [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};

type DBFields = {
  id: { format: "incrementing" };
  name: { type: string; pii: true };
};

type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
type ObjectsNeedingGDPRDeletion = {
    id: false;
    name: true;
}

더 알아보기