TypeScript 배우기 - 12. 매핑된 타입

·

3 min read

TypeScript는 마치 매크로 언어처럼 기존 타입을 기반으로 새로운 타입을 쉽게 만들 수 있습니다.

매핑된 타입은 사전에 선언되지 않은 속성 타입을 선언하는데 사용하는 인덱스 서명 구문을 기반으로 합니다.

아래는 인덱스 서명 구문으로 표현된 예시입니다.

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

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

JavaScript에서 속성은 a["propertyName"]으로 표현될 수 있으므로 [key: string]: boolean | Horse로 표현할 수 있습니다. 그러므로 conformsdelrodneyboolean 타입을 반환하므로 정상 코드가 됩니다.

매핑된 타입은 in keyof를 사용해서 제네릭 인자의 타입을 순회하여 동일한 속성을 가진 새로운 타입을 정의합니다.

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

다음은 매핑된 타입의 예시입니다.

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

// FeatureFlags의 속성 (함수 포함)을 그대로 받아 반환형을 `boolean`으로 바꿔줌
// type FeatureOptions = {
//    darMode: boolean;
//    newUserProfile: boolean;
// }
type FeatureOptions = OptionsFlags<FeatureFlags>;

매핑 수식어

readonly? 앞에 - 또는 +의 접두사를 붙일 수 있습니다. (+ 접두사는 생략 가능)

// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
  // `-readonly`에 의해 `readonly`가 제거됨
  -readonly [Property in keyof Type]: Type[Property];
};

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

// type UnlockedAccount = {
//     id: string;
//     name: string;
// }
type UnlockedAccount = CreateMutable<LockedAccount>;
// 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 = {
//    id: string;
//    name: string;
//    age: number;
// }
type User = Concrete<MaybeUser>;

as로 키 재매핑

TypeScript 4.1 이상부터 as 절을 이용해서 매핑된 타입의 키를 다시 매핑할 수 있습니다.

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

다음은 템플릿 리터럴 타입과 결합해서 이전 속성에서 새로운 속성 이름을 만드는 예제 코드입니다.

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

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

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

조건부 타입을 통해 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 = {
//     radius: number;
// }
type KindlessCircle = RemoveKindField<Circle>;

string | number | symbol 유니온 타입 뿐만 아니라 다른 유니온 타입에 대해 임의의 매핑을 할 수 있습니다.

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 = {
//      square: (event: SquareEvent) => void;
//      circle: (event: CircleEvent) => void;
// }
type Config = EventConfig<SquareEvent | CircleEvent>

대단하군요. 매크로 언어와 같은 느낌입니다.

추가 탐색

매핑된 타입은 다른 조작 섹션의 기능과 잘 결합하여 작동합니다. 다음은 pii 속성이 리터럴 true로 설정되어 있는지의 유무에 따라 true 또는 false를 반환하는 조건부 타입을 사용하는 매핑된 타입의 예시입니다.

type ExtractPII<Type> = {
// Type의 키를 순회하며
// `{ pii: true }`가 있는 경우 true, 없는 경우 false 타입이 됨
  [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};

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

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