맵드 타입 기반의 유틸리티 타입 2 - Pick, Omit, Record

woodstockwoodstock
4 min read

Pick

Pick<T, K>는 특정 객체 타입으로부터 특정 프로퍼티 만을 골라내는 타입이다.

예를 들어 Pick 타입에 Tname, age가 있는 객체 타입이고 Kname 이라면 결과는 name만 존재하는 객체 타입이 된다.

예제

다음과 같이 옛날에 작성된 포스트가 하나 존재한다고 가정해보자.

interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

const legacyPost: Post = { // ❌
  title: "옛날 글",
  content: "옛날 컨텐츠",
};

이때 legacyPost에 저장되어 있는 게시글은 태그나 썸네일 기능이 추가되기 이전에 만들어진 게시글이라고 가정해보자.

이 변수를 Post 타입으로 설정하면 tags 프로퍼티가 존재하기 때문에 오류가 발생하게 된다.

이 상황을 어떻게 해결해야 해야 할까?

옛날에 작성된 게시글이 몇개나 될 지도 모르기 때문에 일일이 tags를 추가해 줄 수도 없고, 그렇다고 옛 게시글들 만을 위한 타입을 별도로 만들어 줄 수도 없다.

Pick 타입으로 문제 해결하기

const legacyPost: Pick<Post, "title" | "content"> = {
  title: "옛날 글",
  content: "옛날 컨텐츠",
};

// 추출된 타입 : { title : string; content : string }

변수 legacyPost의 타입으로 Pick<Post, "title" | "content">을 정의했다.

따라서 이때 타입변수 T에는 Post가 타입변수 K에는 “title” | “content” 이 각각 할당되어 Post 타입으로부터 “title”과 “content” 프로퍼티만 뽑아낸 객체 타입이 된다.

Pick 타입 구현하기

객체 타입을 변형하는 타입이므로 맵드 타입을 이용해 만들 수 있다.

일단 2개의 타입 변수 T와 K를 사용하는 타입이므로 다음과 같이 정의한다.

type Pick<T, K> = any;

다음으로 T로 부터 K 프로퍼티만 뽑아낸 객체 타입을 만들어야 하므로 다음과 같이 맵드 타입으로 정의한다.

type Pick<T, K> = {
  [key in K]: T[key];
}

마지막으로는 KTkey로만 이루어진 String Literal Union 타입임을 보장해 주어야 한다.

따라서 다음과 같이 제약을 추가해준다.

type Pick<T, K extends keyof T> = {
  [key in K]: T[key];
}



Omit

Omit<T, K>은 특정 객체 타입으로부터 특정 프로퍼티 만을 제거하는 타입이다.

예를 들어 Omit 타입에 Tname, age가 있는 객체 타입이고 Kname 이라면 결과는 name을 제외하고 age 프로퍼티만 존재하는 객체 타입이 된다.

예제

이번에는 제목이 없는(title 프로퍼티가 생략된) 게시글도 존재할 수 있다고 가정해보자.

interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const noTitlePost: Post = { // ❌
  content: "",
  tags: [],
  thumbnailURL: "",
};

title 프로퍼티가 없으면 오류가 발생하게 된다.

Omit 타입으로 문제 해결하기

const noTitlePost: Omit<Post, "title"> = {
  content: "",
  tags: [],
  thumbnailURL: "",
};


Omit 타입 구현하기

먼저 2개의 타입 변수를 사용하는 제네릭 타입이므로 일단 다음과 같이 정의한다.

type Omit<T, K> = any;

그 다음 앞서 Pick 타입에서 했던 것 과 같이 K에 제약을 추가한다.

type Omit<T, K extends keyof T> = any;

그리고 앞서 만든 Pick타입을 이용해 다음과 같이 완성한다.

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

일단 TPost, K‘title’ 이라고 가정해보자.

그럼 이때 keyof T‘title’ | ‘content’ | ‘tags’ | ‘thumbnailURL’이므로 Pick<T, Exclude<keyof T, K>>Pick<Post, Exclude<'title' | 'content' | 'tags' | 'thumbnailURL' , 'title>> 이 된다.

다음으로 Exclude는 2개의 타입 변수를 할당받는데 T로부터 K를 제거한다. 따라서 한번 더 변환하면 다음과 같다.

Pick<Post, 'content' | 'tags' | 'thumbnailURL'>

그럼 결과는 Post에서 content, tags, thubmnailURL 프로퍼티만 존재하는 객체 타입이 된다. 따라서 K에 전달한 ‘title’이 제거된 타입을 얻을 수 있다.



Record

마지막으로 Record<K, V>에 대해 살펴보자.

예제

이번에는 썸네일 기능을 업그레이드 해보자.

다음과 같이 화면 크기에 따라 3가지 버전의 썸네일을 지원한다고 가정하고, Thumbnail 타입을 별도로 정의한다.

type Thumbnail = {
  large: {
    url: string;
  };
  medium: {
    url: string;
  };
  small: {
    url: string;
  };
};

그런데 여기에 watch 버전이 또 추가되어야 한다고 가정한다면, 다음과 같이 똑같이 생긴 프로퍼티를 하나 더 추가해줘야 한다.

type Thumbnail = {
 (...)
  watch: {
    url: string;
  };
};

앞으로 버전이 많아질 수록 계속해서 중복코드가 발생하게 될 것이다.

Record 타입으로 문제 해결하기

다음과 같이 K에는 어떤 프로퍼티들이 있을지 String Literal Union 타입을 할당하고 V에는 프로퍼티의 값 타입을 할당한다.

type Thumbnail = Record<
  "large" | "medium" | "small",
  { url: string }
>;

Record 타입은 K에는 “large” | “medium” | “small”이 할당되었으므로 large, medium, small 프로퍼티가 있는 객체 타입을 정의한다. 그리고 각 프로퍼티 value의 타입은 V에 할당한 { url : stirng } 이 된다.

type ThumbnailLegacy = {
  large: {
    url: string;
  };
  medium: {
    url: string;
  };
  small: {
    url: string;
  };
  watch: {
    url: string;
  };
};

type Thumbnail = Record<
  "large" | "medium" | "small" | "watch",
  { url: string; size: number }
>;


Record 타입 구현하기

Record 타입은 다음과 같이 구현할 수 있다.

type Record<K extends keyof any, V> = {
  [key in K]: V;
};

여기서 K extends keyof anyK에 이상한 타입이 들어올 수 있으니 제약을 걸어주었다.

이 제약은 무슨 타입이 될지는 모르지만 적어도 타입 변수 K에 들어오는 타입은 어떤 객체 타입의 키를 추출해 놓은 유니언 타입이야 라고 알려준 것이다.

0
Subscribe to my newsletter

Read articles from woodstock directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

woodstock
woodstock

안녕하세요! 프론트엔드 개발자 woodstock입니다. 저는 매일 조금씩 발전하고자 하는 마음으로 개발공부를 시작했고, 이 블로그는 그 과정에서 배우고 성장하는 이야기를 담고 있습니다. 여러분의 피드백과 조언은 언제나 환영합니다! 함께 배우고 성장하는 과정을 즐길 수 있기를 기대합니다.