맵드 타입 기반의 유틸리티 타입 2 - Pick, Omit, Record
Pick
Pick<T, K>
는 특정 객체 타입으로부터 특정 프로퍼티 만을 골라내는 타입이다.
예를 들어 Pick
타입에 T
가 name, age
가 있는 객체 타입이고 K
가 name
이라면 결과는 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];
}
마지막으로는 K
가 T
의 key
로만 이루어진 String Literal Union
타입임을 보장해 주어야 한다.
따라서 다음과 같이 제약을 추가해준다.
type Pick<T, K extends keyof T> = {
[key in K]: T[key];
}
Omit
Omit<T, K>
은 특정 객체 타입으로부터 특정 프로퍼티 만을 제거하는 타입이다.
예를 들어 Omit
타입에 T
가 name, age
가 있는 객체 타입이고 K
가 name
이라면 결과는 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>>;
일단 T
는 Post
, 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 any
로 K
에 이상한 타입이 들어올 수 있으니 제약을 걸어주었다.
이 제약은 무슨 타입이 될지는 모르지만 적어도 타입 변수 K
에 들어오는 타입은 어떤 객체 타입의 키를 추출해 놓은 유니언 타입이야 라고 알려준 것이다.
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입니다. 저는 매일 조금씩 발전하고자 하는 마음으로 개발공부를 시작했고, 이 블로그는 그 과정에서 배우고 성장하는 이야기를 담고 있습니다. 여러분의 피드백과 조언은 언제나 환영합니다! 함께 배우고 성장하는 과정을 즐길 수 있기를 기대합니다.