TypeScript 배우기 - 12. 매핑된 타입
TypeScript는 마치 매크로 언어처럼 기존 타입을 기반으로 새로운 타입을 쉽게 만들 수 있습니다.
매핑된 타입은 사전에 선언되지 않은 속성 타입을 선언하는데 사용하는 인덱스 서명 구문을 기반으로 합니다.
아래는 인덱스 서명 구문으로 표현된 예시입니다.
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
JavaScript에서 속성은 a["propertyName"]
으로 표현될 수 있으므로 [key: string]: boolean | Horse
로 표현할 수 있습니다. 그러므로 conforms
의 del
및 rodney
는 boolean
타입을 반환하므로 정상 코드가 됩니다.
매핑된 타입은 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>;