Search
Duplicate
💙

[TS] UnionToIntersection

간단소개
union 타입을 intersection 타입으로 바꾼다고?
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
TypeScript
Scrap
태그
Type
9 more properties

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>에서 TTestA가 아닌 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
복사
UnionToIntersectionDispatchToDispatch를 활용해 간소화한 모습
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
복사
여기서 ab의 타입은 각각 (param: A) => any, (param: B) => any가 됩니다.
(typeof a | typeof b) 타입의 함수를 호출할 때에는 인자로 무엇을 넘길 수 있을까요?
그런 인자가 있다면, 그 인자는 A도 만족하고 B도 만족해야 합니다.
즉, Dispatch<A> | Dispatch<B>Dispatch<A & B>가 됩니다.
이 점을 활용해서 infer 기능을 활용하면 union 타입을 intersection 타입으로 바꿀 수 있습니다.