Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

結論

// 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"
mozisan
カリフォルニアのパートタイマー
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away