LoginSignup
1
1

More than 3 years have passed since last update.

TypeScriptでタプル型をインターセクションに変換する

Last updated at Posted at 2019-10-26

結論

// 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"
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1