객체의 특정 프로퍼티 n개를 다른 타입을 갖는 타입으로 매핑하기

이나리·2022년 6월 28일
0

다음과 같은 타입을 가지고 있다고 가정하겠습니다.

interface RecordItem {
  id: number;
  date: string;
  startTime: string;
  endTime: string;
}

위의 RecordItem 타입을 기반으로 다음과 같은 RecordForm 타입을 만들고 싶습니다.

interface RecordForm {
  date: string;
  startTime: Time; // Time이라는 커스텀 타입이 있다고 가정함.
  endTime: Time;
}

Mapped Type

객체 타입의 경우, MappedType 타입을 활용하면, 하나의 객체 타입으로 여러 가지 타입을 생성할 수 있습니다.

다음은 MappedType 타입을 만드는 과정입니다.
전달한 제네릭 타입 T를 통해 이 제네릭에 해당하는 객체 타입의 프로퍼티를 키로 갖고, 모두 number 타입을 갖도록 만듭니다.

type MappedType<T> = {
  [P in keyof T]: number;
};

타입 분석

그런데, 몇가지 문제가 있습니다.

  1. 생성하려는 객체 타입에는 id 프로퍼티가 존재하지 않습니다.

  2. 생성하려는 객체 타입의 프로퍼티가 모두 1가지 타입만 갖지 않습니다.
    date는 string 타입을 갖지만, startTime, endTimeTime이라는 커스텀 타입을 가져야 합니다.

id 프로퍼티 제외하기

객체 타입의 특정 프로퍼티를 제외하는 방법은 다양합니다. 그 중에서 몇가지만 소개하겠습니다.

Omit<T, K>

간단하게 유틸리티 타입 Omit 을 이용해, id 프로퍼티를 제외한 타입을 생성할 수 있습니다.
Omit<T, K>T 의 모든 프로퍼티를 가져온 다음, K 에 정의된 모든 프로퍼티 키를 제외합니다.

type OmitId = Omit<RecordItem, 'id'>;

Exclude<T, U>

Mapped Types 타입과 유틸리티 타입인 Exclude를 조합하여 id 프로퍼티를 제외할 수도 있습니다.
Exclude<T, U>U 에 정의된 모든 프로퍼티를 T 로부터 제외합니다.

type ExcludeId<T> = {
  [P in keyof T as Exclude<P, 'id'>]: T[P]
};

이제 id 프로퍼티를 제외했으니, 나머지 프로퍼티에 타입을 할당해줘야 합니다.
그런데 모두 타입이 다릅니다. 이럴 때는 extends 키워드를 활용하여 타입을 제한할 수 있습니다.

extends 키워드를 이용한 조건부 타입(Conditional Types)

extends 키워드는 A extends B 라고 했을 때, AB 에 할당 가능함을 의미합니다.

T extends { length: number }
T extends number

그래서 이 키워드를 조건부 타입과 같이 사용할 경우, 다음과 같이 사용할 수 있습니다.

interface Animal {}
interface Dog extends Animal {}

type animal = Dog extends Animal ? number : string;

Dog 타입이 Animal 타입에 할당 가능하면, animalnumber 타입이 되고, 그렇지 않으면 string 타입이 됩니다.

조건부 타입은 일반적으로 제네릭 타입과 함께 사용할 때, 더 유용합니다.

type Message<T> = T extends { message: string } ? T['message'] : T;

interface Person {
  message: 'hi';
}

interface Computer {
  calculate: void;
}

type PersonMessage = Message<Person>; // 타입 결과: 'hi'
type ComputerMessage = Message<Computer>; // 타입 결과: Computer

제네릭 타입 T{ message: string } 에 할당 가능하면, Tmessage 프로퍼티 타입을, 그렇지 않으면 전달한 T 타입을 사용합니다.

분기에 따라 사용할 수 있는 타입을 제한하기 때문에, 다른 타입을 가질 수 있게 할 수 있습니다.

타입 생성해보기

위의 방법을 이용하여, 객체 타입의 프로퍼티 키에 따라 원하는 타입을 리턴할 수 있도록 타입을 만들어보겠습니다.

먼저, 2개의 제네릭 타입을 타입에 추가합니다.

첫번째 제네릭은 전달할 객체의 타입이 될 것이고, 두번째 재네릭은 그 객체에서 변경할 프로퍼티가 될 것입니다.

두번째 제네릭의 경우, 프로퍼티를 하나만 전달한다면 string 타입이 되고, 여러 개의 프로퍼티 타입을 변경할 거라면 유니온 타입이 될 수 있습니다.
또한, 첫번째에 전달한 객체 타입의 프로퍼티여야만 하니까 extends 키워드를 사용하여 타입을 제한해야 합니다.

type Change<T, U extends keyof T> = {};

이런 식으로 하면, 두번째 제네릭인 U 에 오는 타입이 T 타입에 전달된 객체의 프로퍼티로만 지정할 수 있습니다.

이제 특정 프로퍼티에만 Time 이라는 커스텀 타입을 전달해야 하는데요.
이때 아까 소개해드린 Mapped Type과 extends 키워드를 활용한 조건부 타입을 사용할 수 있습니다.

먼저, Mapped Type 을 이용해 첫번째 전달한 제네릭 타입 T 의 프로퍼티 P 를 가져옵니다.

그리고 이 P 가 두번째 제네릭인 U 에 할당 가능하다면, 즉 변경하고 싶은 프로퍼티라면 원하는 특정 타입을 리턴하도록 하고, 그렇지 않으면 기존 값을 리턴하도록 하면 됩니다.
이때, 위의 상황을 extends 키워드를 활용하여 조건문 형태로 분기를 만듭니다.

조금 복잡해보이지만, 코드로 구현해보면 아래와 같이 작성할 수 있습니다.

type Change<T, U extends keyof T> = {
  [P in keyof T]: P extends U ? Time : T[P];
}

type RecordForm = Change<RecordItem, 'startTime' | 'endTime'>;

위의 예제는 id 프로퍼티를 제외하지 않았으니, id 프로퍼티를 제외하도록 해보면 다음과 같이 작성할 수 있습니다.

type Change<T, U extends keyof T> = {
  [P in keyof T]: P extends U ? Time : T[P];
}

type RecordForm = Change<Omit<RecordItem, 'id'>, 'startTime' | 'endTime'>;

제네릭을 활용하지 않는다면, 아래와 같이 작성할 수도 있습니다.

type TimeFields = 'startTime' | 'endTime';

type RecordForm = {
  [P in keyof RecordItem as Exclude<P, 'id'>]: P extends TimeFields ? Time : RecordItem[P]
}

전체 코드 보기


이와 관련된 다른 예제를 보고 싶다면, 타입스크립트 문서에 공식 예제가 있습니다.
조건부 타입을 이용한 Mapped Type

0개의 댓글