- Published on
유틸리티 타입을 직접 만들어보자
- Authors
- Name
- 윤영서
유틸리티 타입이란
유틸리티 타입
이란 타입을 변환하거나 조작하는데 사용할 수 있는 타입입니다. 자바스크립트에서 내장 메소드를 제공하는 것 과 같이 타입스크립트에서도 타입 변환을 용이하게 하기 위해서 유틸리티 타입을 제공합니다. 타입스크립트 유틸리티 타입에 대해 이해하기 위해 직접 유틸리티 타입을 만들어 보겠습니다.
Partial<T>
Partial
은 제네릭 타입 T
를 받아 T
의 모든 속성을 선택적으로 만드는 유틸리티 타입입니다.
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
T의 각 키를 순회하는 P를 사용하여, T의 각 속성(T[P])을 선택적으로 만들면 됩니다.
Required<T>
Required
는 제네릭 타입 T
를 받아 T
의 모든 속성을 필수로 만드는 유틸리티 타입입니다.
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
-?
는 선택적 속성을 필수로 만들어주는 문법입니다.
Readonly<T>
Readonly
는 제네릭 타입 T
를 받아 T
의 모든 속성을 읽기 전용으로 만드는 유틸리티 타입입니다.
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
readonly
는 속성을 읽기 전용으로 만들어주는 문법입니다. 이를 타입으로 가지는 객체 속성의 값을 바꾸려 한다면 아래와 같은 에러가 발생합니다.
Record<K, T>
Record
는 제네릭 타입 K
와 T
를 받아 K
의 모든 속성을 T
타입으로 만드는 유틸리티 타입입니다. 보통 객체의 키-값 쌍의 타입을 정의할 때 사용합니다.
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
여기서 keyof any
는 가능한 모든 타입의 키를 의미합니다. 타입스크립트 4.8이후 버전부터는 이를 PropertyKey
로 대체할 수 있습니다. PropertyKey
는 keyof any
와 동일하게 string | number | symbol
을 의미하지만 조금 더 명확한 의미를 가지고 있습니다.
그렇지만 실제로 내부적으로는 keyof any
를 사용하고 있는데, 왜 PropertyKey
로 변경하지 않을까요?
이에 대한 논의가 있었는데, 이미 유틸리티 타입을 제공하기도 하고 PropertyKey
를 사용하기엔 하위 호환성 보장이 어려우므로 개발자들이 커스텀하여 사용하길 권장하고 있습니다.
많은 논의 끝에, 우리는 라이브러리가 자체적으로 'Omit'(그리고 'Exclude'와 다른 관련 타입들)의 '더 엄격한' 버전을 제공하는 것이 더 바람직하다고 생각합니다. 이러한 타입들의 Strict 버전은 상위 제약 조건을 강제하는 면에서 매우 전염성이 강하며, 사람들이 이들 사이에서 현명하게 선택할 것인지는 불분명합니다. 이는 신중하게 관리되지 않으면 실제로 마찰을 일으킬 수 있습니다. 이는 'Omit'이 라이브러리에 포함된 지금 더욱 그렇습니다 - 만약 우리가 'OmitStrict'를 선택한다면 (이에 대해서도 여러 정의 중에서 선택해야 할 것입니다), 사용자 공간에 좋은 이름이 많이 남아있지 않을 것입니다. 이러한 모든 헬퍼 타입을 작성하는 방법은 여러 가지가 있으며, 개발자들이 자신의 '엄격함'의 정의에 맞는 것을 선택하도록 하는 것이 개인의 자유를 최대화하는 선택이 될 것입니다. 여기와 트위터에서 우리의 'Omit' 정의 선택(제 의견으로는 완전히 방어 가능한)에 대한 반응을 보면, 왜 우리가 'OmitStrict'를 선택하는 것에 대해 그다지 흥분하지 않는지 알 수 있습니다. 그것은 개발자의 30-70%를 화나게 할 것입니다. 왜냐하면 우리가 그들이 선호하는 정의를 선택하지 않기 때문입니다. 라이브러리에 무언가를 추가하는 것은 그 정의가 완전히 모호하지 않을 때만 좋은 아이디어인 것 같습니다. 그리고 'OmitStrict'는 그 기준에 맞지 않습니다. 돌이켜 보면, 'Omit'도 그렇지 않았고, 아마도 우리는 그것을 그냥 빼놓았어야 했을 것입니다. 개발자 여러분, TypeScript가 그 이름을 밟아버리지 않을 것이라는 것을 알고 여러분이 선택한 'OmitStrict'의 정의와 함께 행복하고 안전하게 나아가세요. 😬
Pick<T, K>
Pick
은 제네릭 타입 T
와 K
를 받아 T
의 속성 중 K
에 해당하는 속성만을 선택하는 유틸리티 타입입니다.
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
당연하게도 K는 T의 키를 상속받아야 하므로 K extends keyof T
로 제약을 걸어줍니다.
Exclude<T, U>
Exclude
는 제네릭 타입 T
와 U
를 받아 T
에서 U
에 할당할 수 있는 타입을 제외한 타입을 구성합니다.
type MyExclude<T, U> = T extends U ? never : T;
여기서 보면 타입을 만들때도 삼항연산자를 사용할 수 있음을 알 수 있습니다. T
가 U
에 할당할 수 있는 타입이라면 never
를 반환하고, 그렇지 않다면 T
를 반환합니다. 위 타입에서 일어나는 과정은 실제로 T가 U를 순회하는 것이 아니지만 조건부 타입과 분배 법칙이 작동하는 방식 때문에 순회하는 것처럼 보입니다.
MyExclude<'a' | 'b' | 'c', 'a' | 'b'>
// 분배 법칙에 따라 다음과 같이 평가됩니다:
('a' extends 'a' | 'b' ? never : 'a') |
('b' extends 'a' | 'b' ? never : 'b') |
('c' extends 'a' | 'b' ? never : 'c')
Omit<T, K>
Omit
은 T에서 모든 프로퍼티를 선택한 다음 K를 제거한 타입을 구성합니다. Pick
은 프로퍼티를 선택하는 반면, Omit
은 프로퍼티를 제외합니다.
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Omit
은 앞서 언급한 Pick
과 Exclude
를 조합하여 만들 수 있습니다. T에서 K를 제외한 타입을 Pick
으로 선택하면 됩니다. Omit
과 Exclude
는 모두 제외한다는 특성에서 비슷하게 생각될 수 있지만, Omit
은 객체에서, Exclude
는 유니온 타입에서 사용됩니다.
Extract<T, U>
Extract
은 제네릭 타입 T
와 U
를 받아 T
에서 U
에 할당할 수 있는 타입을 추출한 타입을 구성합니다. Exclude
와 반대되는 동작을 합니다. Exclude
가 조건에 맞지 않는 타입을 제거한다면, Extract
는 조건에 맞는 타입만 남깁니다.
type MyExtract<T, U> = T extends U ? T : never;
NonNullable<T>
NonNullable
은 제네릭 타입 T
를 받아 T
에서 null
과 undefined
를 제외한 타입을 구성합니다.
type MyNonNullable<T> = T extends null | undefined ? never : T;
Parameters<T>
Parameters
은 제네릭 타입 T
를 받아 함수 타입 T
의 매개변수 타입을 튜플로 만드는 유틸리티 타입입니다.
type MyParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P
: never;
(...args: any) => any
는 함수의 타입을 의미합니다. 그리고 infer
키워드를 사용하여 함수의 매개변수 타입을 추출할 수 있습니다. infer
은 조건부 타입에서 사용됩니다. 위 코드는 'T가 어떤 매개변수든 받고 어떤 값이든 반환하는 함수 타입인 경우, 그 매개변수들의 타입을 P로 추론하라'는 의미입니다.
ConstructorParameters<T>
ConstructorParameters
은 제네릭 타입 T
를 받아 생성자 함수 타입 T
의 매개변수 타입을 튜플로 만드는 유틸리티 타입입니다.
type MyConstructorParameters<T extends new (...args: any) => any> = T extends new (
...args: infer P
) => any
? P
: never;
앞서 (...args: any) => any
는 함수의 타입을 의미한다고 했습니다. 생성자 함수 타입은 new
키워드를 사용하여 표현합니다.
ReturnType<T>
ReturnType
은 제네릭 타입 T
를 받아 함수 타입 T
의 반환 타입을 구성하는 유틸리티 타입입니다.
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
반환타입이 R로 추론되면 R을 반환하고, 그렇지 않다면 any를 반환합니다.
InstanceType<T>
InstanceType
은 제네릭 타입 T
를 받아 생성자 함수 타입 T
의 인스턴스 타입을 구성하는 유틸리티 타입입니다.
type MyInstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (
...args: any
) => infer R
? R
: any;
InstanceType
은 ConstructorParameters
과 비슷한데 abstract
키워드가 추가되었습니다. abstract
키워드는 추상 클래스를 의미합니다. 즉, T가 추상 클래스를 포함한 모든 생성자 함수 타입을 받을 수 있음을 의미합니다.
ThisParameterType<T>
ThisParameterType
는 제네릭 타입 T
를 받아 함수 타입 T
의 this
매개변수 타입을 구성하는 유틸리티 타입입니다. 명시적 this 인자가 없으면 unknown을 반환합니다.
type MyThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
OmitThisParameter<T>
OmitThisParameter
는 제네릭 타입 T
를 받아 함수 타입 T
의 this
매개변수를 제외한 함수 타입을 구성하는 유틸리티 타입입니다. ThisParameterType
과 다르게 이름에 Type은 없습니다.
type MyOmitThisParameter<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : T;
우선 함수의 매개변수 타입들을 A로 추론합니다. 그리고 반환 타입을 R로 추론합니다. 그리고 추론된 매개변수 타입들을 사용하여 함수 타입을 만듭니다. 함수 타입이면 this 매개변수를 제외한 새 함수 타입을 반환하고, 아니면 원래 타입을 그대로 반환합니다.
ThisType<T>
ThisType
은 제네릭 타입 T
를 받아 this
타입을 T
로 만드는 유틸리티 타입입니다. 특이한 점은 이 타입은 유틸리티는 변형된 타입을 반환하지 않는다는 점입니다. 이 타입은 문맥적 this 타입에 표시하는 역할을 합니다. 이 유틸리티를 사용하기 위해서는 --noImplicitThis
플래그를 사용해야 합니다.
ThisType
은 단지 lib.d.ts에 선언된 빈 인터페이스입니다.
interface ThisType<T> {}
선언 파일에서 단순히 위와 같이 정의되어 있습니다.
Awaited<T>
Awaited
는 제네릭 타입 T
를 받아 Promise
타입 T
의 반환 타입을 구성하는 유틸리티 타입입니다.
type Awaited<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F): any }
? F extends (value: infer V, ...args: any) => any
? Awaited<V>
: never
: T;
T가 null | undefined인 경우 T를 그대로 반환하고, 만약 T가 객체이고 then 메소드를 가지고 있다면 then 메소드의 첫 번째 매개변수(onfulfilled 콜백)의 타입을 F로 추론합니다. 이를 통해 Promise와 유사한 thenable객체를 모두 처리합니다. F(onfulfilled 콜백)가 함수인 경우, 그 함수의 첫 번째 매개변수 타입을 V로 추론합니다. 그리고 V를 Awaited로 재귀적으로 호출하여 반환합니다.
Intrinsic String Manipulation Types
템플릿 문자열 리터럴에서의 문자열 조작을 돕기 위해 구현된 타입 집합입니다.
// Uppercase<StringType>
type Uppercase<T extends string> = intrinsic;
// Lowercase<StringType>
type Lowercase<T extends string> = intrinsic;
// Capitalize<StringType>
type Capitalize<T extends string> = intrinsic;
// Uncapitalize<StringType>
type Uncapitalize<T extends string> = intrinsic;
내부를 확인하면 intrinsic
이라고만 되어 있습니다. 이는 컴파일러에 의해 특별히 처리된다는 것을 의미합니다.