Mapped Type

이재윤·2022년 8월 6일
0

TypeScript

목록 보기
1/2
post-thumbnail

💻 문제 상황

아래와 같은 타입이 있다고 가정해 보겠습니다.

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,
}

위의 경우 필요한 타입은 선언해 주었지만 TransportMediaTypepropertytype이 중복된다는 문제가 있습니다.
Mapped Type을 사용하면 이런 문제를 해결할 수 있습니다.

💻 Mapped Type 이란?

기존 객체의 타입을 기반으로 새로운 객체 타입으로 연결(Mapped)시켜주는 제네릭 타입 입니다.

💻 Mapped Typed의 형태

{[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 typeReadonly, Partial, Required과 같습니다.

💻 Key Remapping via 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
// }

0개의 댓글