type UnionToIntersection<T> = (
T extends unknown ? (x: T) => void : never
) extends (x: infer R) => void
? R
: never;
type Format320 = { urls: { format320p: string } };
type Format480 = { urls: { format480p: string } };
type Format720 = { urls: { format720p: string } };
type Format1080 = { urls: { format1080p: string } };
type Video = Format320 | Format480 | Format720 | Format1080;
const video1: Video = {
urls: {
format320p: "https://...",
},
}; // ✅
const video2: Video = {
urls: {
format320p: "https://...",
format480p: "https://...",
},
}; // ✅
const video3: Video = {
urls: {
format1080p: "https://...",
},
}; // ✅
// never
type FormatKeys = keyof Video["urls"];
위의 코드에서 FormatKeys 타입은 타입 내에서 교차(intersect)하는 공통의 키(key)가 없기 때문에 never 타입이 된다.
type Naked<T> =
T extends ... // naked!
type Unnaked<T> =
{ o: T } extends ... // not naked!
Naked 타입이란 T라는 타입을 무언가로 감싸지 않고 그 자체가 서브 타입 조건에 있는지 검증한다는 것을 의미한다.
type Naked<T> =
T extends unknown ? { o: T } : never;
type Foo = Naked<string | number | boolean>;
type Foo = Naked<string> | Naked<number> | Naked<boolean>;
type Foo =
| string extends unknown ? { o: string } : never
| number extends unknown ? { o: number } : never
| boolean extends unknown ? { o: boolean } : never;
type Foo = { o: string } | { o: number } | { o: boolean };
type Unnaked<T> =
{ o: T } extends unknown ? { o: T } : never;
type Bar = Unnaked<string | number | boolean>;
type Bar = { o: string | number | boolean } extends unknown ?
{ o: string | number | boolean } : never;
type Bar = { o: string | number | boolean };
Naked 타입의 경우 T가 유니온이라면 분산 조건부 타입(distributive conditional types)이 적용되어 유니온 타입에 적용된 조건을 조건 타입의 유니온으로 변환하게 된다.
type ToUnionOfFunction<T> =
T extends unknown ? (x: T) => void : never;
type Phase1 = ToUnionOfFunction<
{ a: string } | { b: string }
>;
type Phase2 =
| ToUnionOfFunction<{ a: string }>
| ToUnionOfFunction<{ b: string }>;
type UnionOfFunctions =
| ((x: { a: string }) => void)
| ((x: { b: string }) => void);
declare const foo: UnionOfFunctions;
// "b" is missing
foo({ a: "hello" });
// "a" is missing
foo({ b: "world" });
// OK
foo({ a: "hello", b: "world" });
여기서 핵심은 UnionOfFunction 타입을 갖는 함수 foo를 안전하게 호출하려면 함수의 요구 사항을 모두 만족하는 타입({ a: string, b: string })의 값을 전달해야 한다는 것이다.
UnionOfFunctions extends (x: infer R) => void
? R // { a: string } & { b: string }
: never;
그 다음으로 하는 작업은 함수 타입을 이용해 타입 T를 래핑하였다가 다시 언래핑하는 작업인데, 함수 인수를 통해 이러한 작업을 수행하는 이유는 추론된 타입 R이 반변(contravariant) 타입이 되기 때문이다.
declare let str: string;
declare let strOrNum: string | number;
strOrNum = str; // ✅ string | number(상위 타입)에 string(하위 타입) 할당 가능
type Func<X> = (...args: X[]) => void;
declare let f: Func<string>;
declare let g: Func<string | number>;
g = f; // 💥 string | number(상위 타입)에 string(하위 타입) 할당 불가
함수 인수에 대해서는 상위 타입(super type)에 하위 타입(sub type)을 할당할 수 없다. 따라서 타입스크립트 입장에서는 반변하게 동작해야 하는 함수 인수에서 해당 타입을 추론(infer)해야 하므로 유니온 타입에서 인터섹션 타입으로 변환하여 추론하는 것이다.
type ToUnionOfFunction<T> = T extends unknown
? (x: T) => void
: never;
type UnionToIntersection<T> = ToUnionOfFunction<T> extends (
x: infer R
) => void
? R
: never;
type Intersected = UnionToIntersection<Video["urls"]>;
type Intersected = UnionToIntersection<
| { format320p: string }
| { format480p: string }
| { format720p: string }
| { format1080p: string }
>;
type Intersected =
| UnionToIntersection<{ format320p: string }>
| UnionToIntersection<{ format480p: string }>
| UnionToIntersection<{ format720p: string }>
| UnionToIntersection<{ format1080p: string }>;
type Intersected =
(x: { format320p: string }) => void extends
(x: infer R) => void ? R : never |
(x: { format480p: string }) => void extends
(x: infer R) => void ? R : never |
(x: { format720p: string }) => void extends
(x: infer R) => void ? R : never |
(x: { format1080p: string }) => void extends
(x: infer R) => void ? R : never;
// infer R
type Intersected =
{ format320p: string } &
{ format480p: string } &
{ format720p: string } &
{ format1080p: string };
// "format320p" | "format480p" | "format720p" | "format1080p"
type FormatKeys = keyof UnionToIntersection<Video["urls"]>;
TypeScript: Union to intersection type
Explain Like I'm Five: TypeScript UnionToIntersection type
Transform union type to intersection type
type-fest/union-to-intersection.d.ts at main · sindresorhus/type-fest