Search
🔌

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

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

Parameters<Type>

Parameters하나의 Function 타입을 인자로 받아서, 해당 Function 타입의 매개변수들의 Tuple 타입을 반환한다.
type Parameters<T> = ...
TypeScript
음... 일단 조금 어려워졌다. 우선 우리가 해볼 수 있는 부분은 인자로 받아야 하는 타입이 Function 타입이어야 한다는 것이다. 타입 문법에서 Function 타입은 (numberArg: number) => string 과 같이 쓸 수 있다. 우리가 받아야 하는 건 ‘아무’ Function 타입이다. Function 타입 중 가장 상위에 있는 ‘아무’ 타입이란, 매개변수가 있든 없든 몇 개이든 상관없고, 그 타입도 상관없고, 리턴 타입도 void이든 number이든 아무 상관 없는, 그런 Function 타입이 아닐까? 다음과 같이 쓸 수 있겠다.
type Parameters<T extends (...args: any) => any> = ...
TypeScript
T를 Function 타입으로 잘 받아왔다면, 이제 T의 매개변수 부분만 쏙 빼 와서 새로운 타입을 construct하면 된다.
‘쏙 빼온다’? 왠지 infer 키워드가 떠오르지 않는가? 그렇다. 삼항 연산자를 이용해 조건절에 T가 함수 타입인 것을 다시 한번 확인하면서 ...args 부분을 Pinfer하고, 조건이 맞다면 P를, 그렇지 않다면 never를 반환하면 된다.
그 어려워 보이던 Parameters 타입도 우리 손으로 직접 짤 수 있게 되었다.
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any : P : never
TypeScript

ConstructorParameters<Type>

ConstructorParametersConstructor function 타입을 인자로 받아서, 해당 Constructor function의 매개변수들의 Tuple 타입을 반환한다.
...사실 그냥 Function이 아니라 Constructor function이 되었을 뿐이지, 로직 자체는 동일하다. 인자 T가 Constructor function인지만 체크할 수 있다면 Parameters를 그대로 갖다 쓸 수 있는 수준이다.
type ConstructorParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any : P : never
TypeScript
해당 함수가 Constructor인지 체크하는 가장 쉬운 방법은? 역시나 new 키워드를 붙여보는 것이다.
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any : P : never
TypeScript
사실 여기까지만 해도 잘 작동할 텐데, TypeScript 최신 버전부터는 abstract Constructor가 추가되면서 다음과 같이 Utility type 정의가 조금 바뀌었다.
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never
TypeScript
그 이유는 TypeScript 4.2에 추가된 abstract Construct Signatures 때문이다. Abstract constructor types와 Non-abstract constructor types를 구분하기 시작하면서 abstract constructor 또한 ConstructorParameters에서 지원해야 할 일이 생겼고, 그 때문에 abstract 키워드가 붙은 것으로 이해하면 된다.
그러면 Non-abstract constructor type은 ConstructorParameters를 사용할 수 없는가? 그렇지 않다. Non-abstract constructor types은 Abstract constructor types에 대입이 가능하고, 반대는 불가능하다.

ReturnType<Type>

ReturnTypeFunction 타입을 인자로 받아서, 이의 반환 타입을 반환한다.
일단 얘도 함수를 받는다.
type ReturnType<T extends (...args: any) => any> = ...
TypeScript
그리고 삼항 연산자의 조건문에서 extends로 함수인지 한번 더 체크하면서, 이번에는 반환하는 타입을 infer로 빼 와서 사용하면 되겠다.
너무 쉽다.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
TypeScript
자세히 살펴보면 알겠지만, ReturnType의 경우에만 삼항 연산자의 false 부분이 never가 아닌 any이다. 명확한 이유를 찾진 못했지만, 그렇게 중요한 부분도 아닐 뿐더러 ReturnType<any>의 경우에 any를 반환하기 위해서가 아닐까 추측한다.

InstanceType<Type>

InstanceTypeConstructor function 타입을 인자로 받아서, 해당 Constructor function이 반환하는(생성하는) 인스턴스의 타입을 반환한다.
위 과정을 이해하면서 따라왔다면 이것도 너무 쉽다. Constructor function 타입을 받는 부분은 ConstructorParameters를 만들면서, 반환 타입을 infer하는 부분은 ReturnType을 만들면서 직접 해 봤기 때문이다. 이제 둘을 적절히 합치면 된다.
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any
TypeScript

ThisParameterType<Type>

ThisParameterTypeFunction 타입을 인자로 받아서, 해당 Function의 명시적 this 인자 타입을 반환한다. 명시적 this 인자가 없으면 unknown을 반환한다.
Parameters를 조금만 응용하면 되겠다. ParametersT extends (...args: infer P) => any : P : never처럼 함수의 모든 인자 ...argsinfer했는데, 지금은 this 파라미터만 infer해오면 되는 상황이다.
따라서 다음과 같이 함수 매개변수 맨 앞의 this만 특정하여 infer로 가져와서 반환하고, this 뒤의 나머지 매개변수들은 사용하지 않는 것으로 쉽게 구현할 수 있다.
type ThisParameterType<T> = T extends (this: infer U, ...args: any) => any ? U : unknown
TypeScript

OmitThisParameter<Type>

마지막이다!
OmitThisParameter인자로 받은 타입에서 this 파라미터를 제거한 새로운 타입을 반환한다.
만약 인자로 받은 타입에 this 파라미터가 없다면, 인자로 받은 타입을 그대로 반환한다.
this 파라미터가 있다면, this 파라미터를 제외한 새로운 Function 타입을 만들어서 반환한다.
우선 첫 번째 과정부터 해 보자. T에 this 파라미터가 없다면, T를 그대로 반환한다. 이는 바로 위에서 만들었던 ThisParameterType 유틸리티 타입을 이용해 쉽게 체크할 수 있다. ThisParameterType<T>T에 this 파라미터가 없으면 unknown을 반환하므로, T에 this 파라미터가 없는지에 대한 여부는 unknown extends ThisParameterType<T> 조건으로 체크할 수 있다.
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : ...
TypeScript
만약 this 파라미터가 있어서 삼항 연산자의 false절로 넘어왔다면? this 파라미터를 제외한 새로운 Function 타입을 반환해주면 된다.
어떻게 명시적 this 파라미터를 제거할까? 간단하다. this를 명시하지 않으면 된다. 바로 위에서 만든 ThisParameterType을 다시 보면, (this: infer U, ...args: any[]) => any와 같이 명시적으로 this 파라미터를 infer해 왔는데, (...args: infer A) => any와 같이 this를 명시하지 않으면 TypeScript는 this를 제외한 매개변수들만 Ainfer해온다.
따라서 함수의 Arguments 타입과 Return 타입을 각각 infer해서 새로운 함수를 만들어 리턴해주면 된다. this를 명시하지 않음으로써 this 파라미터를 제거한 것이다.
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T
TypeScript
TypeScript에서의 this에 대한 더 자세한 동작은 https://www.typescriptlang.org/docs/handbook/2/functions.html#declaring-this-in-a-function 을 참고하자.

마치며

확실히 제네릭 타입의 기능만 보고 이를 직접 작성해 보는 것은 타입 작성 능력에 굉장한 도움을 준다고 생각한다. 그리고 그 중 가장 쉽게 할 수 있는 것이 바로 TypeScript에 내장된 Utility Types를 직접 작성해 보는 것이라고 생각한다.
TypeScript 4.6 기준으로 내장된 모든 Utility Types를 직접 작성하는 과정을 써 보았다. 누군가에게는 너무 쉽게 풀어썼다고 느껴질지도, 누군가에게는 이조차도 따라가기 너무 어렵다고 느껴질지도 모르겠다. 그래도 TypeScript 마스터를 꿈꾸는 ‘누군가’에게 이 글이 도움이 되었으면 좋겠다.
TypeScript를 마스터하는 그날까지 화이팅!