結論
// https://qiita.com/suin/items/93eb9c328ee404fdfabc
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
declare const boxedBrand: unique symbol;
type Boxed<T> = { [boxedBrand]: T };
type BoxedValueOf<T> = T extends Boxed<infer U> ? U : never;
type TupleToBoxedTuple<T> = { [K in keyof T]: Boxed<T[K]> };
type TupleToBoxedUnion<T> = TupleToBoxedTuple<T>[keyof T];
type TupleToIntersection<T> = BoxedValueOf<UnionToIntersection<TupleToBoxedUnion<T>>>;
type A = TupleToIntersection<['hoge', string]>;
// A = "hoge"
解説
型変換は、調べると「タプルからユニオン」とか「ユニオンからインターセクション」は出てくるのですが、「タプルからインターセクション」に変換する方法はなかなか見つかりません。
(ググラビリティの問題?)
いやでも待てよと、「タプルからユニオン」「ユニオンからインターセクション」に連続して変換すればタプルからインターセクションが出てくるじゃん!と思った私は騙されました。
次のような場合に想定しないことになってしまいます。
type A = UnionToIntersection<TupleToUnion<[string, 'hoge']>>;
// A = string
// 実際に得られる型
type B = string & 'hoge';
// B = "hoge"
// 本来ほしいのはこの型
なぜならば、一度'hoge' | string
というユニオンに変換された時点でstring
という型になってしまうからです。
'hoge'
や0
といったリテラル型は、string
型やnumber
型のようなより広い型に吸収されてしまうようです。
そこでなんとかリテラル型が吸収されないようにしながらユニオンに変換し、そのあとにインターセクションに変換するという手順を踏みます。
まずは、次のような型を用意します。
declare const boxedBrand: unique symbol;
type Boxed<T> = { [boxedBrand]: T };
これは型をラップするための型で、こいつを使うとユニオンを用いても型が吸収されるのを防げます。
type A = Boxed<'hoge'> | Boxed<string>;
// A = Boxed<"hoge"> | Boxed<string>
Boxed<T>
型からT
型を取り出すための型も用意しておきます。
type BoxedValueOf<T> = T extends Boxed<infer U> ? U : never;
type A = BoxedValueOf<Boxed<'hoge'>>;
// A = "hoge"
ポイントはこのBoxedValueOf<T>
で、もはやネタバラシではありますが次のような型の取り出しが可能です。
type A = BoxedValueOf<Boxed<{ hoge: true }> & Boxed<{ fuga: true }>>
// A = {
// hoge: true;
// } & {
// fuga: true;
// }
Boxed<A> & Boxed<B>
という型に対してBoxedValueOf<T>
を適用すると、A & B
というインターセクションが得られます。
すなわち、
「タプルからユニオン」「ユニオンからインターセクション」
このフローではなく「タプルからBoxedユニオン」「BoxedユニオンからBoxedインターセクション」「Boxedインターセクションから生のインターセクション」という流れで型を変換することで、求めていた「タプルからインターセクション」の型変換を行うことができます。
まとめると、次のようなコードで「タプルからインターセクション」の型変換が可能です。
// https://qiita.com/suin/items/93eb9c328ee404fdfabc
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
declare const boxedBrand: unique symbol;
type Boxed<T> = { [boxedBrand]: T };
type BoxedValueOf<T> = T extends Boxed<infer U> ? U : never;
type TupleToBoxedTuple<T> = { [K in keyof T]: Boxed<T[K]> };
type TupleToBoxedUnion<T> = TupleToBoxedTuple<T>[keyof T];
type TupleToIntersection<T> = BoxedValueOf<UnionToIntersection<TupleToBoxedUnion<T>>>;
type A = TupleToIntersection<['hoge', string]>;
// A = "hoge"