💡 유틸리티 타입이란
유틸리티 타입이란 타입스크립트가 자체적으로 제공하는 특수한 타입입니다. 제네릭, 맵드 타입, 조건부 타입 등의 타입 조작 기능을 이용하여 자주 사용되는 유용한 타입들을 모아 놓은 것을 의미합니다.
이번 게시글에서 살펴볼 유틸리티 타입들은 다음과 같습니다.
💡 맵드 타입 기반의 유틸리티 타입
• Partial<T>
Partial은 부분적인 또는 일부분의 라는 뜻으로, Partial<T>는 특정 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔주는 타입입니다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const draft: Post = { // ❌ tags 프로퍼티가 없습니다.
title: "제목은 미정",
content: "초안...",
};
게시글을 표현하는 타입 Post를 선언하였고, Post 타입의 임시 저장 게시글 변수 draft가 있다고 가정해보았습니다. 현재 Post 타입의 title, tags, content 프로퍼티는 필수 프로퍼티인데 변수 draft에는 tags 프로퍼티가 존재하지 않아 오류가 발생하였습니다. 다음과 같이 게시글의 일부 정보가 아직 설정되어 있지 않은 임시 저장 게시글의 경우에도 변수에 저장할 수 있어야 하는데 해당 변수를 Post 타입으로 정의해버리면 오류가 발생하게 되는 것입니다. 그렇다고 임시 저장 게시글 기능을 위해 Post 타입의 모든 프로퍼티를 선택적 프로퍼티로 설정하는 것 또한 좋지는 않습니다. 이럴 때에 Partial<T> 타입으로 문제를 해결할 수 있습니다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const draft: Partial<Post> = {
title: "제목은 미정",
content: "초안...",
};
Partial<T> 타입은 타입 변수 T로 전달한 객체 타입의 모든 프로퍼티를 모두 선택적 프로퍼티로 변환합니다. 따라서 Partial<Post> 타입은 모든 프로퍼티가 선택적 프로퍼티가 된 Post 타입과 같습니다.
type Partial<T> = {
[key in keyof T]?: T[key];
};
이번에는 Partial<T> 유틸리티 타입을 직접 구현핻보았습니다. T에 할당된 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔줘야하는데 기존 객체 타입을 다른 타입으로 변환하는 타입은 맵드 타입이었습니다. 따라서 맵드 타입과 함께 유틸리티 타입을 구현해주었습니다.
• Required<T>
Required는 필수의, 필수적인 이라는 뜻으로, Required<T>는 특정 객체 타입의 모든 프로퍼티를 필수 프로퍼티로 바꿔주는 타입입니다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const withThumbnailPost: Post = {
title: "타입스크립트 후기",
tags: ["ts"],
content: "",
// thumbnailURL: "https://...", // 선택적 프로퍼티라서 이 코드를 삭제하여도 에러 발생 X
};
이번에는 썸네일이 필수적으로 존재해야하는 게시글 변수 withThumbnailPost를 정의하였습니다. 하지만 Post 타입의 thumbnailURL 프로퍼티가 현재 선택적 프로퍼티로 설정되어 있기 떄문에 다음과 같이 마지막 줄 코드를 주석 처리하거나 삭제하여도 타입 오류가 발생하지는 않습니다.
이때, 타입의 프로퍼티 속성을 변경하지 않고 이 변수에 한정해 thumbnailURL을 필수 프로퍼티로 만들어 주고 싶은 경우에는 어떻게 해야 할까요? 바로 Required<T> 타입으로 문제 해결이 가능합니다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const withThumbnailPost: Required<Post> = { // ❌
title: "타입스크립트 후기",
tags: ["ts"],
content: "",
// thumbnailURL: "https://...",
};
Required<Post>는 Post 타입의 모든 프로퍼티가 필수 프로퍼티로 변경된 객체 타입입니다. 따라서 위 코드처럼 thumbnailURL 프로퍼티를 생략하면 오류가 발생하게 됩니다. thumbnailURL 같은 선택적 프로퍼티도 필수 프로퍼티로 바꿔주는 역할을 하는 것이죠.
type Required<T> = {
[key in keyof T]-?: T[key];
};
Required<T> 타입을 직접 구현해보았습니다. 모든 프로퍼티를 필수 프로퍼티로 만든다는 의미는 모든 프로퍼티에서 '선택적(?)'이라는 기능을 제거(-)하는 것과 같습니다. 따라서 다음과 같이 -?를 프로퍼티 이름 뒤에 붙여주면 됩니다.
-? 는 ?가 붙어 있는 선택적 프로퍼티의 ?를 제거하라는 의미입니다.
• Readonly<T>
Readonly는 읽기 전용이라는 뜻으로, Readonly<T>는 특정 객체 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 만들어주는 타입입니다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const readonlyPost: Post = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = "보호를 해제합니다.";
이번에는 내부를 수정할 수 없는 보호된 게시글 변수 readonlyPost를 Post 타입으로 정의해보았습니다. 하지만 Post 타입의 모든 프로퍼티가 모두 readonly 설정이 되어 있지 않기 때문에 마지막 줄 코드처럼 내부 값을 변경할 수 있습니다. 이러한 문제를 해결하기 위해 사용하는 것이 Readonly<T> 입니다.
const readonlyPost: Readonly<Post> = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = ""; // ❌
Readonly<Post>는 Post 타입의 모든 프로퍼티를 readonly(읽기전용) 프로퍼티로 변환합니다. 따라서 점 표기법을 이용하여 특정 프로퍼티의 값을 수정하려고 하면 오류가 발생합니다.
type Readonly<T> = {
readonly [key in keyof T]: T[key];
};
Readonly<T>도 구현해보았습니다.
• Pick<T, K>
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를 정의하였습니다. 이때 legacyPost에 저장되어 있는 게시글은 태그나 썸네일이 추가되기 이전에 작성된 게시글이라고 가정해보겠습니다. 그런데 이 변수를 Post 타입으로 설정하면 필수 프로퍼티인 tags 가 변수에는 정의되어있지 않아서 오류를 발생시킵니다.
이와 같이 과거에 작성된 모든 게시물들을 일일이 수정해주는 대대적인 수정작업을 거치기에는 많은 노력이 들 것입니다. 이때, Pick<T, K>를 사용하면 문제를 손쉽게 사용할 수 있습니다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const legacyPost: Pick<Post, "title" | "content"> = {
title: "과거글",
content: "과거 컨텐츠",
};
변수 legacyPost의 타입으로 Pick<Post, "title" | "content"를 정의하였습니다. 따라서 타입변수 T에는 Post가, 타입변수 K에는 "title" | "content"이 각각 할당됩니다. 그러면 Post 타입으로부터 "title"과 "content" 프로퍼티만 골라서 뽑아낸 객체 타입이 됩니다.
type Pick<T, K extends keyof T> = {
[key in K]: T[key];
};
Pick<T, K>의 타입을 구현해보았습니다. T로부터 K의 프로퍼티만을 뽑아낸 객체 타입을 만들어야하므로 다음과 같이 맵드 타입으로 정의하면 됩니다. 추가적으로 K가 T의 key로만 이루어진 String Literal Union 타입임을 보장해주어야 합니다. 따라서 다음과 같이 extends keyof 키워드를 추가해주었습니다.
•Omit<T, K>
Omit은 생략하다라는 의미로, Omit<T, K>는 특정 객체 타입으로부터 특정 프로퍼티만을 제거하는 타입입니다. 예를 들어 Omit 타입에 T가 name, age가 있는 객체 타입이고 K가 name이라면 결과는 name을 제외한 age 프로퍼티만 존재하는 객체 타입이 됩니다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const noTitlePost: Post = { // ❌
content: "",
tags: [],
thumbnailURL: "",
};
이번에는 제목이 없는 게시글도 존재할 수 있다고 가정하고 변수 noTitlePost를 정의해주었습니다. 현재 이 변수의 타입에는 Post 타입이 정의되어 있는데 필수 프로퍼티인 title이 존재하지 않아 오류가 발생하고 있습니다.
const noTitlePost: Omit<Post, "title"> = {
content: "",
tags: [],
thumbnailURL: "",
};
다음과 같이 Omit을 이용하여 Post 타입으로부터 title 프로퍼티를 제거한 타입으로 변수의 타입을 정의해주면 문제가 해결됩니다. Omit 대신에 Pick<Post, "content" | "tags" | "thumbnailURL">를 사용할 수도 있지만 프로퍼티의 양이 방대해지면 이마저도 부담스러운 코드의 길이가 될 수 있기 때문에 이러한 상황에서는 Omit을 사용하는 것을 추천합니다.
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
Omit<T, K>를 구현한 코드입니다. 변수 noTitlePost에서 사용한 Omit 구문을 예시로 설명하겠습니다.
⓵ T = Post, K = "title"
⓶ Pick<Post, Exclude<keyof Post, "title">>
⓷ Pick<Post, Exclude<"title" | "content" | "tags" | "thumbnailURL", "title">>
⓸ Pick<Post, "content" | "tags" | "thumbnailURL">
다음과 같은 과정으로 Post에서 content, tags, thumbnailURL 프로퍼티만 존재하는 객체 타입, 즉 K 타입변수에 전달한 title 프로퍼티만 제거된 타입을 얻을 수 있습니다.
•Record<K, V>
type Thumbnail = {
large: {
url: string;
};
medium: {
url: string;
};
small: {
url: string;
};
// 추가하려는 프로퍼티
watch: {
url: string;
};
};
화면의 크기에 따라 3가지 버전의 썸네일을 지원하는 Thumbnail 타입을 정의해주었습니다. 그런데 여기에 watch 버전이 또 추가되어야한다고 가정하면 다음과 같이 똑같이 생긴 프로퍼티를 하나 더 추가해주어야하며, 중복코드가 발생하게 됩니다. 이럴 때 바로 Record를 이용하면 됩니다.
type Thumbnail = Record<"large" | "medium" | "small", { url: string; }>;
type Thumbnail = Record<"large" | "medium" | "smaill" | "watch", { url: string; size: number }>;
다음과 같이 String Literal Union 타입을 할당하고 V에는 프로퍼티의 값을 할당합니다. 위 Record 타입은 K에는 "large" | "medium" | "small"이 할당되었으므로 large, medium,small 프로퍼티가 있는 객체 타입을 정의합니다. 그리고 각 프로퍼티 value의 타입은 V에 할당한 { url: string }이 됩니다. 두번째 줄 코드에서 처럼 추가적으로 정의하고 싶은 프로퍼티와 value 또한 추가로 지정해줄 수 있습니다.
type Record<K extends keyof any, V> = {
[key in K]: V;
};
Record<K, V> 타입을 구현하였습니다. K extends keyof any는 타입변수 K에 들어오는 타입은 어떤 객체 타입의 키를 추출해놓은 유니온 타입이라는 의미입니다.
💡 조건부 타입 기반의 유틸리티 타입
•Exclude<T, U>
Exclude 타입은 T로부터 U를 제거하는 타입입니다.
type A = Exclude<string | boolean, boolean>; // string
type Exclude<T, U> = T extends U ? never : T; // 구현체
Exclude 타입은 다음과 같은 과정을 거칩니다.
⓵ Exclude<string, boolean> | Exclude<boolean, boolean>
⓶ string | never
⓷ string (공집합 never 제거됨)
•Extract<T, U>
Extract 타입은 T로부터 U를 추출하는 타입입니다.
type B = Extract<String | boolean, boolean>; // boolean
type Extract<T, U> = T extends U ? T : never; // 구현체
• ReturnType<T>
ReturnType은 타입변수 T에 할당된 함수 타입의 반환값 타입을 추출하는 타입입니다.
type ReturnType<T extends (...args: any) => any> = T extends (...args: 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
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 조건부 타입 이해하기: 제네릭, 분산적 조건부, infer (0) | 2024.12.23 |
---|---|
[ TypeScript ] 타입 조작하기 2 - 맵드 타입, 템플릿 리터럴 타입 (0) | 2024.12.23 |
[ TypeScript ] 타입 조작하기 1 - 인덱스드 액세스 타입, keyof & typeof 연산자 (0) | 2024.12.23 |
[ TypeScript ] 제네릭으로 확장하기: 인터페이스, 클래스, 프로미스 (0) | 2024.12.22 |
[ TypeScript ] 제네릭 이해하기: 타입 변수와 메서드 타입 정의(map, forEach) (1) | 2024.12.22 |