
유틸리티 타입이란 타입스크립트가 제공하는제네릭, 맵드 타입, 조건부 타입등의 타입 조작 기능을 이용해 실무에서 자주 사용되는 타입들을 만들어 놓은 특수한 타입들이다.

Partial은 부분적인 또는 일부분의 라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 변환한다. 따라서 기존 객체 타입에 정의된 프로퍼티들 중 일부분만 사용할 수 있도록 도와주는 타입이다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const draft: Post = { // ❌ tags 프로퍼티가 없음
title: "제목은 나중에 짓자...",
content: "초안...",
};
글을 쓸때는 모든 값이 필요하지만 게시글의 일부 정보가 아직 설정되어 있지 않은 임시 저장 게시글의 경우에도 변수에 저장할 수 있어야 하는데 해당 변수를 Post 타입으로 정의하면 오류가 발생한다. 임시 저장 게시글 기능을 위해 Post 타입의 모든 프로퍼티를 선택적 프로퍼티로 설정하는 것도 곤란한데, 진짜 작성이 완료되어 화면에 렌더링 될 게시글들은 이 모든 프로퍼티를 진짜 다 가지고 있어야 한다.
이런 상황에 다음과 같이 Partial 유틸리티 타입을 이용하면 된다!
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const draft: Partial<Post> = {
title: "제목 나중에 짓자",
content: "초안...",
};
Partial<T> 타입은 타입 변수 T로 전달한 객체 타입의 모든 프로퍼티를 다 선택적 프로퍼티로 변환한다. 따라서 Partial 타입은 모든 프로퍼티가 선택적 프로퍼티가 된 Post 타입과 같다.
모든 타입을 받을 수 있게 먼저 를 선언한다.
그리고 T에 할당된 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔줘야 한다. 기존 객체 타입을 다른 타입으로 변환하는 타입은 맵드 타입이다. 따라서 맵드 타입을 이용해 다음과 같이 수정한다.
type Partial<T> = {
[key in keyof T]?: T[key];
};
Required는 우리말로 필수의, 필수적인 이라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 필수(선택적이지 않은) 프로퍼티로 변환한다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
// 반드시 썸네일 프로퍼티가 존재해야 하는 게시글
const withThumbnailPost: Post = {
title: "한입 타스 후기",
tags: ["ts"],
content: "",
thumbnailURL: "https://...",
};
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const withThumbnailPost: Required<Post> = { // ❌
title: "한입 타스 후기",
tags: ["ts"],
content: "",
// thumbnailURL: "https://...",
};
Required는 Post 타입의 모든 프로퍼티가 필수 프로퍼티로 변환된 객체 타입이다. 따라서 위 코드처럼 thumbnailURL 프로퍼티를 생략하면 이제 오류가 발생하게 된다.
Partial 타입과 거의 유사한데 일단 기존의 모든 프로퍼티를 포함하는 제네릭 맵드 타입으로 만들어 주고,
type Required<T> = {
[key in keyof T]: T[key];
};
그리고 나서 이제 모든 프로퍼티가 필수 프로퍼티가 되도록 만들면 된다.
모든 프로퍼티를 필수 프로퍼티로 만든다는 말은 반대로 바꿔보면 모든 프로퍼티에서 ‘선택적’ 이라는 기능을 제거하는 것 과 같다. 따라서 다음과 같이 -?를 프로퍼티 이름 뒤에 붙여주면 된다.
type Required<T> = {
[key in keyof T]-?: T[key];
};
-? 는 ?가 붙어있는 선택적 프로퍼티가 있으면 ?를 제거하라는 의미이다.
마지막으로 살펴볼 유틸리티 타입은 Readonly 이다. Readonly는 우리말로 읽기 전용 이라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 변환한다.
절대 내부를 수정할 수 없는 보호된 게시글이 하나 필요하다고 가정하면,
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const readonlyPost: Post = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = '해킹당함';
Readonly는 Post 타입의 모든 프로퍼티를 readonly(읽기 전용) 프로퍼티로 변환한다. 따라서 점표기법을 이용해 특정 프로퍼티의 값을 수정하려고 하면 오류를 발생시킨다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const readonlyPost: Readonly<Post> = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = '해킹당함'; // ❌
type Readonly<T> = {
readonly [key in keyof T]: T[key];
};
Pick은 특정 객체 타입으로부터 특정 프로퍼티 만을 골라내는 그런 타입이다. 예를 들어 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을 사용하면 된다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
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” 프로퍼티만 쏙 뽑아낸 객체 타입이 된다.
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가 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을 사용하여 Post 타입으로부터 title 프로퍼티를 제거한 타입으로 변수의 타입을 정의해 주면 된다.
const noTitlePost: Omit<Post, "title"> = {
content: "",
tags: [],
thumbnailURL: "",
};
그 다음 앞서 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’이 제거된 타입을 얻을 수 있다.
아래와 같이 화면 크기에 따라 3가지 버전의 썸네일을 지원한다고 가정하고,Thumbnail 타입을 별도로 정의한다.
type Thumbnail = {
large: {
url: string;
};
medium: {
url: string;
};
small: {
url: string;
};
};
그런데 여기에 watch 버전이 또 추가되어야 한다고 가정하면 똑같이 생긴 프로퍼티를 하나 더 추가해줘야 합니다. 앞으로 버전이 많아질 수록 계속해서 중복코드가 발생하게 될 것이다. 이럴 때 바로 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 Record<K extends keyof any, V> = {
[key in K]: V;
};
Exclude 타입은 다음과 같이 T로부터 U를 제거하는 타입다.
type A = Exclude<string | boolean, string>;
// boolean
Exclude를 구현해보면 아래와 같다.
type Exclude<T, U> = T extends U ? never : T;
Extract 타입은 다음과 같이 T로 부터 U를 추출하는 타입이다
type B = Extract<string | boolean, boolean>;
// boolean
Extract를 구현해보면 아래와 같다.
type Extract<T, U> = T extends U ? T : never;
ReturnType은 타입변수 T에 할당된 함수 타입의 반환값 타입을 추출하는 타입이다.
function funcA() {
return "hello";
}
function funcB() {
return 10;
}
type ReturnA = ReturnType<typeof funcA>;
// string
type ReturnB = ReturnType<typeof funcB>;
// number
ReturnType을 구현해보면 아래와 같다.
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: never;
실제로 실무에서 Omit이나 Pick을 쓰는거 보고 이게 뭐지..? 하면서 눈치껏 썼었는데 정확하게 어떻게 쓰는지, 구현은 어떤식으로 이루어지는지 알게되어서 뿌듯^_^ 하다.
유틸리티타입이 실제로 실무나 프로젝트에서 젤 많이 쓰게 될 타입같아서 잊지않고 열심히 사용해야겠다~!
타입스크립트 기본중에 기본 완료~!!