아래와 같은 타입이 있다고 가정해 보겠습니다.
type TransportMediaType = {
kind: "video" | "audio",
track: MediaStreamTrack,
enabled: boolean,
volum: number,
}
만약 위의 타입을 기준으로 모든 property
들을 optional로 지정한 타입이 필요하거나, 변경이 불가능하게 설정한 타입이 필요한 상황이 생겼습니다.
이 경우에 필요한 타입을 다음과 같이 선언할 수 있습니다.
type PartialTransportMediaType = {
kind?: "video" | "audio",
track?: MediaStreamTrack,
enabled?: boolean,
volum?: number,
}
type ReadonlyTransportMediaType = {
readonly kind: "video" | "audio",
readonly track: MediaStreamTrack,
readonly enabled: boolean,
readonly volum: number,
}
위의 경우 필요한 타입은 선언해 주었지만 TransportMediaType
의property
와 type
이 중복된다는 문제가 있습니다.
Mapped Type을 사용하면 이런 문제를 해결할 수 있습니다.
기존 객체의 타입을 기반으로 새로운 객체 타입으로 연결(Mapped)시켜주는 제네릭 타입 입니다.
{[Property in K]: Type}
in
키워드의 경우는 JS의 for...in
과 유사합니다. K
의 모든 타입을 순회하면서 해당하는 타입을 Property
에 할당한다고 생각하면 됩니다.
readonly
나 ?
, -
, +
등과 같은 modifier
가 붙을 수 있지만 기본적인 형태는 위와 같습니다.
위에서 보았던 TransportMediaType
타입에 Mapped type을 적용해 보면 아래와 같습니다.
type M1 = {[P in "video" | "audio"]: MediaStreamTrack}
// {video: MediaStreamTrack; audio: MediaStreamTrack}
type M2 = {[P in "video" | "audio"]: P}
// {video: "video"; audio: "audio"}
type M3 = {[P in "kind" | "track"]: TransportMediaType[P]}
// {kind: "video" | "audio"; track: MediaStreamTrack}
type M4 = {[P in keyof TransportMediaType]: TransportMediaType[P]}
// TransportMediaType과 동일
처음에 보았던 문제를 Mapped Type을 적용하여 해결해 보겠습니다.
먼저 PartialTransportMediaType
의 경우 아래와 같이 나타낼 수 있습니다.
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type PartialTransportMediaType = MyPartial<TransportMediaType>
위 타입 선언문의 동작 원리는 다음과 같습니다.
1. keyof
로 타입 T
의 모든 key들을 가져 옵니다.
2. in
으로 타입 T
의 key를 순회합니다.
3. 해당하는 key갑을 대입해 타입을 생성합니다.
다음으로 ReadonlyTransportMediaType
에 적용해 보겠습니다.
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
type ReadonlyTransportMediaType = MyReadonly<TransportMediaType>
추가로 TransportMediaType
이 없는 상황에서, ReadonlyTransportMediaType
으로 부터 TransportMediaType
을 만들어야 한다면 아래와 같이 만들 수 있습니다.
type MyRequired<T> = {
[P in keyof T]-?: T[P]
}
type TransportMediaType = MyRequired<ReadonlyTransportMediaType>
❗️위에서 소개한 mapped type의 예시는 utility type의 Readonly
, Partial
, Required
과 같습니다.
as
TS 4.1 부터 as
를 이용해서 key remapping을 할 수 있습니다.
type MappedTypeWithNewProperties<T> = {
[P in keyof T as NewKeyType]: T[P]
}
객체의 getter
함수의 타입을 지정할 때 key remapping을 사용할 수 있습니다.
type Getter<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
}
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
❗️Captialize
의 경우는 string
타입만 적용가능 하므로 string
이 아닌 타입을 필터링 하기 위해 intersection
타입을 지정해 주었습니다.
특정 key를 필터링한 새로운 타입을 만들때도 사용할 수 있습니다.
type RemoveKindField<T> = {
[P in keyof T as Exclude<T, "kind">]: T[P]
}
interface Circle {
kind: "circle",
radius: "number
}
type KindlessCircle = RemoveKindField<Circle>;
// {
// radius: number
// }
string | number | symbol
외에도 다른 타입의 union
을 사용할 수 있습니다.
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>
// {
// square: (event: square) => void,
// circle: (evnet: circle) => void
// }
Conditional Type과 조합해서 사용할 수도 있습니다.
type ExtractPII<T> = {
[P in keyof T]: T[P] extends {pii: true} ? true : false;
}
type DBFields = {
id: {format: "incrementing"},
name: {type: string; pii: true}
}
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>
// {
// id: false,
// name: true
// }