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
를 추가해 줄 수도 없고, 그렇다고 옛 게시글들 만을 위한 타입을 별도로 만들어 줄 수도 없다.
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”
프로퍼티만 뽑아낸 객체 타입이 된다.
객체 타입을 변형하는 타입이므로 맵드 타입을 이용해 만들 수 있다.
일단 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<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
프로퍼티가 없으면 오류가 발생하게 된다.
const noTitlePost: Omit<Post, "title"> = {
content: "",
tags: [],
thumbnailURL: "",
};
먼저 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<K, V>
에 대해 살펴보자.
이번에는 썸네일 기능을 업그레이드 해보자.
다음과 같이 화면 크기에 따라 3가지 버전의 썸네일을 지원한다고 가정하고, Thumbnail
타입을 별도로 정의한다.
type Thumbnail = {
large: {
url: string;
};
medium: {
url: string;
};
small: {
url: string;
};
};
그런데 여기에 watch
버전이 또 추가되어야 한다고 가정한다면, 다음과 같이 똑같이 생긴 프로퍼티를 하나 더 추가해줘야 한다.
type Thumbnail = {
(...)
watch: {
url: string;
};
};
앞으로 버전이 많아질 수록 계속해서 중복코드가 발생하게 될 것이다.
다음과 같이 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
타입은 다음과 같이 구현할 수 있다.
type Record<K extends keyof any, V> = {
[key in K]: V;
};
여기서 K extends keyof any
로 K
에 이상한 타입이 들어올 수 있으니 제약을 걸어주었다.
이 제약은 무슨 타입이 될지는 모르지만 적어도 타입 변수 K
에 들어오는 타입은 어떤 객체 타입의 키를 추출해 놓은 유니언 타입이야 라고 알려준 것이다.