조건부 타입에서의 분산 추론 억제
조건부 타입
타입스크립트는 특정 조건에 따라 타입을 다르게 정의할 수 있도록 하는 '조건부 타입'을 지원한다. 자바스크립트의 삼항 연산자와 유사하게 타입 수준에서 조건을 평가하며, 그 결과에 따라 다른 타입을 선택할 수 있다. infer 키워드와 함께 사용하면 함수나 배열, 객체 등에서 특정 타입을 추출하거나 분해하는 데 도움이 된다.
아래의 예시 타입은 배열을 인자로 받아서 해당 배열의 첫번째 아이템을 리턴하는 함수의 타입을 조건부 타입과 infer 키워드를 사용해 설계한 것이다.
type FirstElement = <T extends [any, ...any[]]>(arr: T) => T extends [infer First, ...any[]] ? First : never;
const getFirstElement: FirstElement = (arr) => {
return arr[0]
}
const a = getFirstElement(["text", false]); // a의 타입은 string
const b = getFirstElement([42, "hello"]); // b의 타입은 number
조건부 타입에서의 분산 추론
타입스크립트는 유니언 타입에 대해 조건부 타입을 분배하여 각 구성원에 대해 개별적으로 평가하고, 이를 다시 유니언 타입으로 반환한다. 이를 영어로는 Distributive Conditional Types라고 부르며, 한국어로는 여러 방식으로 번역되는 듯하다. 나는 이를 '조건부 타입에서의 분산 추론'이라고 부르는 편이다.
타입스크립트의 내장 유틸리티 타입 중에서는 Exclude와 Extract 타입이 분산 추론을 활용하도록 구현되어있다.
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
조건부 타입에서의 분산 추론 억제
아주 가끔은 이러한 분산 추론 현상을 억제하고 유니언 타입을 통채로 조건부 타입 추론에 사용해야 하는 경우가 있다. 이런 경우 타입을 튜플로 묶어주면 타입스크립트가 타입을 개별적으로 분산하여 추론하지 않게 된다.
어떤 어떤 타입 T가 어떤 타입 U와 완벽하게 동일한지를 판별하는 커스텀 유틸리티 타입 Exact가 필요하다고 해보자. 이 경우 T extends U와 U extends T를 동시에 확인해야 한다. 따라서 기본적인 로직은 아래와 같을 것이다. 아래의 Exact 타입은 단일 타입 간의 비교에서는 아무런 문제가 발생하지 않는다.
하지만 유니언 타입을 사용하게 되면 의도와 다르게 동작하게 된다. 타입 B의 경우 string 타입과 string | number 타입이 완벽하게 동일하지 않지만, never가 아닌 string으로 타입 추론되는 모습을 확인할 수 있다. 이는 U extends T를 처리하는 과정에서 유니언 타입을 분산 추론하기 때문이다.
type Exact<T, U> = T extends U ? (U extends T ? T : never) : never
type A = Exact<string, number> // never
type B = Exact<string, string | number> // string
따라서 Exact 타입이 올바르게 동작하기 위해서는 조건부 타입에서의 분산 추론을 억제해야 한다. 앞에서 말한 것처럼 이런 경우 타입을 튜플로 묶어주는 것으로 분산 추론을 억제할 수 있다.
type Exact<T, U> = [T] extends [U] ? ([U] extends [T] ? T : never) : never
type A = Exact<string, number> // never
type B = Exact<string, string | number> // never
type C = Exact<string | number, string | number> // string | number
블로그의 정보
Ayden's journal
Beard Weard Ayden