📍 https://www.typescriptlang.org/docs/handbook/utility-types.html
Documentation - Utility Types
Types which are globally included in TypeScript
www.typescriptlang.org
1. 유틸리티 타입이란?
- 유틸리티 타입이란 타입스크립트가 자체적으로 제공하는 특수한 타입들이다.
- 제네릭, 맵드 타입, 조건부 타입등의 타입 조작 기능을 이용해 실무에서 자주 사용되는 유용한 타입들을 모아 놓은 것이다.
💡 가장 자주 활용되는 유틸리티 타입
2. 맵드 타입 기반
2.1 Partial<T>
- 특정 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 변환
- 기존 객체 타입에 정의된 프로퍼티들 중 일부분만 사용할 수 있도록 도와주는 타입이다.
🚨 문제
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const draft: Post = { // ❌ tags 프로퍼티가 없음
title: "제목은 나중에 짓자...",
content: "초안...",
};
- 예를들어 간단한 블로그 플랫폼의 일부를 구현한다고 가정하고 게시글 하나를 표현하는 타입을 정의한다.
- 임시 저장 기능을 필요로 하다고 가정하고 임시 저장된 게시글을 변수로 저장할 수 있게 해본다.
- 이때 게시글의 일부 정보가 아직 설정되어 있지 않은 임시 저장 게시글의 경우에도 변수에 저장할 수 있어야 하는데 해당 변수를 Post 타입으로 정의하면 오류가 발생한다.
- 임시 저장 게시글 기능을 위래 Post 타입의 모든 프로퍼티를 선택적 프로퍼티로 설정하기에는 실제로 작성이 완료되어 화면에 렌더링 될 게시글들은 이 모든 프로퍼티를 다 가지고 있어야 하기 때문이다.
💡 해결
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const draft: Partial<Post> = {
title: "제목 나중에 짓자",
content: "초안...",
};
- 이런 상황에는 Partial<T> 유틸리티 타입을 이용하면 된다.
- Partial<T> 타입은 타입 변수 T로 전달한 객체 타입의 모든 프로퍼티를 다 선택적 프로퍼티로 전달한다.
- 따라서 Partial<Post> 타입은 모든 프로퍼티가 선택적 프로퍼티가 된 Post 타입과 같다.
👩🏼💻 구현
Partial<T>를 직접 구현해보자.
1) 하나의 타입 변수 T를 사용하는 제네릭 타입이다.
type Partial<T> = any;
2) T에 할당된 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔줘야한다.
type Partial<T> = {
[key in keyof T]?: T[key];
};
- 기존 객체 타입을 다른 타입으로 변환하는 타입은 맵드 타입이다.
- 따라서 맵드 타입을 이용해 Partial<T>를 구현할 수 있다.
2.2 Required<T>
- 특정 객체 타입의 모든 프로퍼티를 필수 프로퍼티로 변환
🚨 문제
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
(...)
// 반드시 썸네일 프로퍼티가 존재해야 하는 게시글
const withThumbnailPost: Post = {
title: "한입 타스 후기",
tags: ["ts"],
content: "",
thumbnailURL: "https://...",
};
- 앞에 본 문제에 이어 이번에는 썸네일이 반드시 있어야 하는 게시글이 하나 필요하다고 가정해보자.
- withThumbnailPost는 빈드시 썸네일이 포함되어야 하는 게시글이어야 하는데 Post 타입의 thumbnailURL 프로퍼티가 선택적 프로퍼티로 설정되어 있기 때문에 주석 처리하거나 삭제한다고 해도 타입 오류가 발생하지 않는다.
- thumbnailURL 을 필수 프로퍼티로 만들어 주고 싶다면 어떻게 해야할까?
💡 해결
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
(...)
const withThumbnailPost: Required<Post> = { // ❌
title: "한입 타스 후기",
tags: ["ts"],
content: "",
// thumbnailURL: "https://...",
};
- 이때 Required<T> 타입을 이용하면 된다.
- Required<Post>는 Post 타입의 모든 프로퍼티가 필수 프로퍼티로 변환된 객체 타입이다.
- 따라서 이제 thumbnailURL를 생략하면 오류가 발생하게 된다.
👨🏼💻 구현
Required<T>를 직접 구현해보자. Partial<T> 타입과 거의 유사하다.
1) 기존의 모든 프로퍼티를 포함하는 제네릭 맵드 타입으로 만들어 본다.
type Required<T> = {
[key in keyof T]: T[key];
};
2) 모든 프로퍼티가 필수 프로퍼티가 되도록 만들어야 한다. 즉, 선택적 기능을 제거하는 것으로 프로퍼티 이름 뒤에 -?를 붙여주면 된다.
type Required<T> = {
[key in keyof T]-?: T[key];
};
- -? 는 ? 가 붙어있는 선택적 프로퍼티가 있으면 ?를 제거하라는 의미이다.
2.3 Readonly
- 특정 객체 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 변환
🚨 문제
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
(...)
const readonlyPost: Post = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = '해킹당함';
- 위 예제에 이어 절대 내부를 수정할 수 없는 보호된 게시글이 하나 필요하다고 가정해보자.
- 변수 readonlyPost 는 보호받아야 하는 게시글로 절대 객체 내부의 값을 수정하지 못하게 막아야 한다.
- 그러나 Post 타입의 모든 프로퍼티가 다 readonly 설정이 안되어 있기 때문에 수정을 방지하지 못한다.
💡 해결
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
(...)
const readonlyPost: Readonly<Post> = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = '해킹당함'; // ❌
- 이때 Readonly<T> 타입을 이용하면 된다.
- Readonly<Post>는 Post 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 변환한다.
- 따라서 점 표기법을 이용해 특정 프로퍼티의 값을 수정하려고 하면 오류를 발생시킨다.
👩🏼💻 구현
Readonly<T>를 직접 구현해보자.
type Readonly<T> = {
readonly [key in keyof T]: T[key];
};
2.4 Pick<T, K>
- 특정 객체 타입으로부터 특정 프로퍼티 만을 골라내는 타입
🚨 문제
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
(...)
const legacyPost: Post = { // ❌
title: "",
content: "",
};
- 위 예제에 이어 옛날에 작성된 포스트가 하나 존재한다고 가정해보자.
- 이때 legacyPost에 저장되어 있는 게시글은 태그나 썸네일 기능이 추가되기 이전에 만들어진 게시글이라고 가정하면 변수를 Post 타입으로 설정했을 때 tags 프로퍼티가 존재하기 때문에 오류가 발생한다.
- 옛날에 작성된 게시글이 몇 개인지 일일이 tags를 추가해 줄 수 없으며 옛 게시글들 만을 위한 타입을 별도로 만들어 줄 수도 없다. 어떻게 해야할까?
💡 해결
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" 프로퍼티만 뽑아낸 객체 타입이 된다.
👨🏼💻 구현
Pick 타입을 직접 구현해보자. 타입을 변형하는 타입이므로 맵드 타입을 이용해 만들 수 있다.
1) 2개의 타입 변수 T와 K를 사용하여 정의한다.
type Pick<T, K> = any;
2) T 로 부터 K 프로퍼티만 뽑아낸 객체 타입을 만들어야 하므로 맵드 타입을 다음과 같이 정의한다.
type Pick<T, K> = {
[key in K]: T[key];
};
3) K가 T의 key로만 이루어진 string literal union 타입임을 보장해줘야 한다. 따라서 제약을 추가한다.
type Pick<T, K extends keyof T> = {
[key in K]: T[key];
};
2.5 Omit<T, K>
- 특정 객체 타입으로부터 특정 프로퍼티 만을 제거하는 타입
🚨 문제
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: "",
};
- Omit을 이용해 Post 타입으로부터 title 프로퍼티를 제거한 타입으로 변수의 타입을 정의해주면 된다.
👩🏼💻 구현
Omit 타입을 직접 구현해보자.
1) 2개의 타입 변수를 사용하는 제네릭 타입으로 정의해본다.
type Omit<T, K> = any;
2) Pick 타입과 같이 K에 제약을 추가한다.
type Omit<T, K extends keyof T> = any;
3) 앞서 만든 Pick 타입을 이용해 완성한다.
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
- T는 Post, K는 "title" 이라고 가정해보자.
- 이때 keyof T는 "title" | "content" | "tag" | "thumbnailURL" 이므로 Pick<T, Exclude<keyof T, K>>은 Pick<Post, Exclude< "title" | "content" | "tag" | "thumbnailURL", "title">>이 된다.
- Exclude는 2개의 변수를 할당받는데 T 로부터 K를 제거한다.
- Pick<Post, "content" | "tag" | " thumbnailURL"> 로 변환된다.
- 결과적으로 Post에서 content, tag, thumbnailURL 프로퍼티만 존재하는 객체 타입이된다.
- 따라서 K에 전달한 "title"이 제거된 타입을 얻을 수 있다.
2.6 Record<K, V>
🚨 문제
type Thumbnail = {
large: {
url: string;
};
medium: {
url: string;
};
small: {
url: string;
};
};
type Thumbnail = {
(...)
watch: {
url: string;
};
};
- 위 예제에 이어 썸네일 기능을 업그레이드하여 화면 크기에 따라 3가지 버전의 썸네일을 지원한다고 가정해보자.
- Thumbnail 타입을 별도로 정의한다.
- 여기에 watch 버전이 또 추가된다고 가정해보면 똑같이 생긴 프로퍼티를 하나 더 추가해줘야 한다. 즉 버전이 많아질 수록 계속 중복 코드가 발생하게 된다.
💡 해결
type Thumbnail = Record<
"large" | "medium" | "small",
{ url: string }
>;
- 이때 Record를 이용하면 된다.
- K에는 어떤 프로퍼티들이 있을지 string literal union 타입을 할당하고 V에는 프로퍼티의 값 타입을 할당한다.
- 위 Record 타입 K에는 "large" | "medium" | "small"이 할당되었으므로 large, medium, small 프로퍼티가 있는 개체 타입을 정의한다.
- 각 프로퍼티 value의 타입은 V에 할당한 { url: string }이 된다.
👨🏼💻 구현
Record 타입을 직접 구현해보자.
type Record<K extends keyof any, V> = {
[key in K]: V;
};
3. 조건부 타입 기반
3.1 Exclude<T, K>
type A = Exclude<string | boolean, string>;
// boolean
- Exclude 타입은 T 로부터 K를 제거하는 타입이다.
👩🏼💻 구현
type Exlcude<T, U> = T extends U ? never : T;
3.2 Extract<T, K>
type B = Extract<string | boolean, boolean>;
// boolean
- Extract 타입은 T 로부터 K를 추출하는 타입이다.
👩🏼💻 구현
type Extract<T, U> = T extends U ? T : never;
3.3 ReturnType<T>
type ReturnType<T extends (...args: any) => any> = T extends (
...agrs: any
) => infer R
? R
: never;
function funcA() {
return "hello";
}
function funcB() {
return 10;
}
type ReturnA = ReturnType<typeof funcA>;
// string
type ReturnB = ReturnType<typeof funcB>;
// number
- ReturnType은 타입변수 T에 할당된 함수 타입의 반환값 타입을 추출하는 타입이다.
📌
'Front-end > TypeScript' 카테고리의 다른 글
[TS] 조건부 타입 (0) | 2024.02.22 |
---|---|
[TS] 타입 조작하기 (0) | 2024.02.22 |
[TS] 제네릭에 대하여 (0) | 2024.02.21 |
[TS] 함수와 타입 (0) | 2024.02.20 |
[TS] 타입을 추론하고... 단언하고... 좁히고... (0) | 2024.02.19 |