UnionToIntersection
A | B같은 union 타입을 받아서 A & B같은 intersection 타입을 만드는 유틸 타입입니다.
type UnionToIntersection<T> =
(T extends any ? (x: T) => any : never) extends
((x: infer R) => any) ? R : never;
TypeScript
복사
UnionToIntersection 타입
이 간단한 타입이 왜 필요한지 소개하고, 의도대로 동작하는 원리를 뜯어보겠습니다.
왜 필요한가
아래 간단한 코드를 봅시다.
interface TestA {
type: 'A';
value: string;
}
interface TestB {
type: 'B';
value: number;
}
interface TestC {
type: 'C';
value: number[];
extra: string;
}
type Test = TestA | TestB | TestC;
declare function test<T extends Test>(type: T['type'], value: T['value']): T;
TypeScript
복사
Test라는 tagged union 타입이 있습니다.
그 태그가 되는 type에 따라 다른 value를 받는 함수 test가 있다고 가정하겠습니다.
문제
test('A', "Hello world!"); // OK
test('A', 42); // Error expected
TypeScript
복사
첫번째 인자로 A를 넣으면 두번째 인자로 string이 들어가는 것을 의도했습니다.
하지만 웬걸? 타입 오류가 발생하지 않습니다!
<T extends Test>에서 T가 TestA가 아닌 Test로 추론되었기 때문입니다.
해결(?) 방법
type TestMap = {
'A': TestA;
'B': TestB;
'C': TestC;
};
declare function test<K extends keyof TestMap>(
type: K,
value: TestMap[K]['value']
): TestMap[K];
TypeScript
복사
tagged union 대신 map 타입을 쓰면 간단히 그 문제를 해결할 수 있습니다.
대신 이 방법은 Test가 바뀔 때마다 TestMap을 개발자가 직접 바꿔줘야 합니다.
개발자는 DRY해야 합니다. Test가 바뀌면 TestMap이 자동으로 바뀌게 할 방법이 있을까요?
DRY한 고-급 해결 방법!
type ToMap<Union, Key extends keyof Union> =
UnionToIntersection<
Union extends { [K in Key]: infer I } ? I extends keyof any ?
{ [K in I]: Union } : never : never>;
type TestMap = ToMap<Test, 'type'>;
TypeScript
복사
UnionToIntersection 타입을 활용한 ToMap 타입을 만들면 이 문제를 쉽게 해결할 수 있습니다!
이렇게 하면 귀찮게 type을 두 번 쓰지 않아도 되고,
실수로 type을 잘못 입력하거나 타입을 하나 빠뜨리는 실수도 방지할 수 있습니다.
원리
이제 UnionToIntersection 타입이 의도대로 잘 동작하는 원리를 뜯어보겠습니다.
간단히 만들기
타입이 보기가 좀 어려운데, 간단한 타입 세 개로 간단히 나누면 이렇게 됩니다.
type Dispatch<T> = (x: T) => any;
type ToDispatch<T> = T extends any ? Dispatch<T> : never;
type UnionToIntersection<T> =
ToDispatch<T> extends Dispatch<infer R> ? R : never;
TypeScript
복사
UnionToIntersection을 Dispatch와 ToDispatch를 활용해 간소화한 모습
Dispatch는 워낙 간단하니 설명이 필요 없을 것 같네요.
ToDispatch도 간단한데, ToDispatch<A | B>는 Dispatch<A | B>가 아니라 Dispatch<A> | Dispatch<B>가 된다는 점만 알고 넘어가면 되겠습니다.
UnionToIntersection의 원리
함수 a는 A 타입을 받고 함수 b는 타입 B를 받는다고 가정하겠습니다.
declare function a(param: A): any;
declare function b(param: B): any;
TypeScript
복사
여기서 a와 b의 타입은 각각 (param: A) => any, (param: B) => any가 됩니다.
(typeof a | typeof b) 타입의 함수를 호출할 때에는 인자로 무엇을 넘길 수 있을까요?
그런 인자가 있다면, 그 인자는 A도 만족하고 B도 만족해야 합니다.
즉, Dispatch<A> | Dispatch<B>는 Dispatch<A & B>가 됩니다.
이 점을 활용해서 infer 기능을 활용하면 union 타입을 intersection 타입으로 바꿀 수 있습니다.