🔌

TypeScript Utility Types를 제로베이스부터 직접 작성해 보자! (1)

Date
2021/05/18
Tags
TypeScript
Type System
Tutorial
Created by
TypeScript에 내장된 Utility Types를 직접 제로베이스부터 작성해보면서 TypeScript의 타입 문법에 익숙해져 보자. (1)
다음 편:
Table of Contents

Prerequisites

이 글을 읽고 보면 이해가 수월하다.

Partial, Required, Readonly

Partial<Type>

Partial하나의 타입을 인자로 받아서, 해당 타입의 모든 프로퍼티를 optional로 설정한 새로운 타입을 생성한다.
타입을 인자로 받아서 프로퍼티에 대해 모두 optional로 설정해주려면 어떻게 하면 될까? 일단 인자로 받은 타입 T의 Key값과 Value값을 가져와서 타입을 다시 재구성하는 코드를 작성해보자.
Key값은 어떻게 가져올 수 있을까? 우리는 keyof 키워드를 알고 있다. keyof 키워드를 통해 인자 T의 모든 Key를 가져올 수 있다.
type Partial<T> = keyof T;
TypeScript
복사
keyof T는 타입 T의 Key의 타입들을 합친 Union type을 반환한다. keyof T에 속하는 모든 타입에 대해서 해당 프로퍼티의 Value를 가져와서 타입을 만들고 싶다면? 아래와 같이 하면 된다.
type Partial<T> = { [P in keyof T]: T[P]; }
TypeScript
복사
위와 같은 문법으로 keyof T의 각 타입을 P로 받아와서, T[P]와 같이 T의 값을 참조하면 된다. 콜론(:) 왼쪽의 문법이 ES2015에 추가된 "Computed property names" 문법과 비슷하다는 걸 알 수 있다.
아직까지 Partial<T>T 그 자체이다. 우리는 이제 각 프로퍼티(Key)를 optional로 설정해주고 싶다. 어떻게 하면 될까? 아래와 같이 프로퍼티 이름 앞에 물음표를 붙여주면 된다.
type Partial<T> = { [P in keyof T]?: T[P]; }
TypeScript
복사
최종적으로는 다음과 같이 Partial Utility Type이 완성된다.
type Partial<T> = { [P in keyof T]?: T[P]; }
TypeScript
복사

Required<Type>

RequiredPartial과 정반대의 결과를 내놓는다. 하나의 타입을 인자로 받아서, 해당 타입의 모든 프로퍼티에 optional을 제거한 새로운 타입을 반환한다.
optional을 제거하려면? 단순히 아래와 같이 ? 대신 -?을 붙여주면 된다. 너무나 간단해서 과정은 생략한다.
type Required<T> = { [P in keyof T]-?: T[P]; }
TypeScript
복사

Readonly<Type>

Readonly하나의 타입을 인자로 받아서, 해당 타입의 모든 프로퍼티에 readonly를 적용한 새로운 타입을 반환한다.
이것까지도 너무 쉽게 할 수 있다. 사실 Partial, Required, Readonly 세 개를 한 번에 설명하려는 것도 이 때문이다.
type Readonly<T> = { readonly [P in keyof T]: T[P]; }
TypeScript
복사

Record<Keys, Type>

Record 유틸리티 타입은 두 개의 타입을 인자로 받는다. KeysType 인자를 받아서 프로퍼티 키는 Keys 타입 중 하나이고 프로퍼티 값은 Type인 오브젝트 타입을 반환한다.
Record를 사용한 예제는 다음과 같다.
interface CatInfo { age: number; breed: string; } type CatName = "miffy" | "boris" | "mordred"; const cats: Record<CatName, CatInfo> = { miffy: { age: 10, breed: "Persian" }, boris: { age: 5, breed: "Maine Coon" }, mordred: { age: 16, breed: "British Shorthair" }, };
TypeScript
복사
자, 어떻게 하면 Record 타입을 만들 수 있을까? 우선 우리가 만들 타입은 오브젝트 타입이라는 것을 알아두자.
type Record<K, T> = { }
TypeScript
복사
그리고, 오브젝트의 각 프로퍼티마다 키는 K 타입 중 하나이고, 값은 T 타입이어야 한다.
type Record<K, T> = { [P in K]: T; }
TypeScript
복사
마지막으로, 오브젝트의 프로퍼티 키로 사용될 수 있는 타입이 string, number, symbol 셋 밖에 없다는 것을 인지하고, 안정성을 더해주면 Record 타입이 완성된다.
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
TypeScript
복사

Exclude, Extract

Exclude<UnionType, ExcludedMembers>

Exclude는 이름에서도 쉽게 알 수 있듯, 인자로 받은 UnionType 타입에서 ExcludedMembers 타입을 제외한 타입을 반환한다. 차집합 개념으로 이해하면 쉬울 것이다.
type Exclude<T, U> =
TypeScript
복사
위 설명을 재정의하면, (유니온)타입 TU가 주어졌을 때, TU에 속하는 타입만 제외한 새로운 타입을 반환하라는 문제가 된다. 이를 그대로 타입 정의 문법으로 풀어내면 아래와 같이 쓸 수 있다.
type Exclude<T, U> = T extends U ? never : T
TypeScript
복사

Extract<Type, Union>

ExtractExclude와 정반대로 작동한다. 인자로 받은 Type 타입과 Union 타입의 교집합을 반환하는 타입이다.
Exclude와 정반대로 작동하므로, 타입 정의 또한 명확하다. 이번에는 TU에 속하는 타입만 골라내면 된다.
type Extract<T, U> = T extends U ? T : never
TypeScript
복사

Pick, Omit

Pick<Type, Keys>

Pick도 두 개의 타입 인자를 받는데, 오브젝트 타입 Type에서 프로퍼티 키가 Keys에 해당하는 프로퍼티들만 모은 새로운 타입을 반환한다.
일단 우리가 만들 타입은 오브젝트 타입이 된다.
type Pick<T, K> = { }
TypeScript
복사
그리고, 오브젝트의 각 프로퍼티마다 키는 K의 타입이어야 하니 일단 이렇게 적어주고,
type Pick<T, K> = { [P in K]: }
TypeScript
복사
K를 하나씩 가져온 P를 키로 하는 프로퍼티의 값은 당연히 원래 오브젝트 타입 TP에 해당하는 타입이어야 하므로 다음과 같이 써 줄수 있다.
type Pick<T, K> = { [P in K]: T[P]; }
TypeScript
복사
마지막으로, K가 오브젝트 타입 T의 프로퍼티 키여야 한다는 것에만 유의하면 완벽한 Pick 타입을 만들 수 있다.
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
TypeScript
복사

Omit<Type, Keys>

Omit은 이론적으로는 Pick과 정반대의 작동을 한다. 오브젝트 타입 Type에서 Keys를 키로 가진 프로퍼티를 제외한 새로운 오브젝트 타입을 반환한다.
일단 또 오브젝트 타입이다.
type Omit<T, K> = { }
TypeScript
복사
이번에는 T의 key에서 K를 제외한 새로운 key 집합에 대해 T[P]를 할당하는 오브젝트 타입을 만들면 되겠다.
type Omit<T, K> = { [P in ?]: T[P]; }
TypeScript
복사
그런데, T의 key에서 K를 제외한 key 집합을 어떻게 만들어낼 것인가? 여기서는 지금까지 나왔던 유틸리티 타입들 중 하나의 도움을 받아야 한다. 바로 Exclude이다.
바로 위의 문장을 그대로 타입 정의문으로 옮기면, Exclude<keyof T, K>와 같이 쓸 수 있다.
type Omit<T, K> = { [P in Exclude<keyof T, K>]: T[P]; }
TypeScript
복사
마지막으로, key 타입이 string, number, symbol밖에 될 수 없다는 것에 유의하면 다음과 같이 타입을 완성할 수 있다.
type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }
TypeScript
복사

NonNullable<Type>

간단한 타입이다. NonNullable 타입은 주어진 Type에서 nullundefined 타입을 제외한 새로운 타입을 반환한다.
위에서 설명한 타입들을 직접 작성해보는 연습을 해 보았다면, 이 정도 타입은 눈 감고도 쓸 수 있을 것이다.
type NonNullable<T> = T extends null | undefined ? never : T
TypeScript
복사

마치며

TypeScript Utility Types에는 이 글에서 소개한 것 외에도 함수 타입을 주로 다루는 Parameters, ReturnType, InstanceType 등이 존재한다. TypeScript 초심자들에게 이것들은 infer 키워드를 비롯해 처음 보는 키워드들과 처음 보는 문법들의 향연이 될 것이므로, 나머지 Utility Types는 다음 편에서 다루도록 하겠다.