C++と色々

主にC++やプログラムに関する記事を投稿します。

Typescriptでタプル型の連結をする

動機

タプル型を連結したくなるときはありませんか?例えば以下のようなケースを考えます

// 2つタプルが存在しています
const t1 = [1, "aaa"] as const; // readonly [1, "aaa"] 型
const t2 = [true, () => {}] as const; // readonly [true, () => void] 型

// 繋げたい
const result = [...t1, ...t2];

ですが、 result はそれぞれのタプル型の情報がなくなり、要素の型のユニオン型の配列になってしまいます

全ての要素の型のユニオン型の配列になってしまう
全ての要素の型のユニオン型の配列になってしまう

これではタプルでせっかくN番目の要素はT型として推論して欲しいのに不便です。そこで、任意のタプルを任意個与えると、それらを要素の型を保持して連結したタプル型を返す型をつくりました。

実装

// ライブラリ側

// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2
type Head<T> = T extends readonly [infer H, ...readonly any[]] ? H : never;
type Tail<T> = T extends readonly any[]
  ? ((...args: T) => any) extends (head: any, ...args: infer R) => any
    ? R
    : never
  : never;

type Add<T extends readonly any[], E> = ((e: E, ...t: T) => any) extends (
  ...a: infer U
) => any
  ? U
  : never;

// (名前適当すぎる...)
type Iter2<T extends readonly any[], Result extends readonly any[] = []> = {
  0: Result;
  1: Iter2<Tail<T>, Add<Result, Head<T>>>;
}[T['length'] extends 0 ? 0 : 1];

// https://stackoverflow.com/questions/54607400/typescript-remove-entries-from-tuple-type
type Iter<
  T extends readonly any[],
  T2 extends readonly any[],
  N extends number,
  Result extends readonly any[] = []
> = {
  0: Result;
  1: Iter<Tail<T>, Tail<T>[0], Tail<T>["length"], Result>;
  2: Iter<T, Tail<T2>, N, Add<Result, Head<T2>>>;
}[T["length"] extends 0 ? 0 : T2["length"] extends 0 ? 1 : 2];

// これを作りました!
type ConcatTuple<T extends readonly any[]> = Iter2<Iter<T, T[0], T["length"]>>;

const concatTuple = <T extends readonly (readonly [...any[]])[]>(...args: T): ConcatTuple<T> => [].concat(...args) as any;
// 比較用にConcatTupleを使ってない普通の連結関数
const concatTuple2 = <T extends readonly (readonly [...any[]])[]>(...args: T)=> [].concat(...args);

// 使う側

const t1 = [1, "aaa"] as const; // readonly [1, "aaa"] 型
const t2 = [true, () => {}] as const; // readonly [true, () => void] 型

const result = concatTuple(t1, t2); // [1, "aaa", true, () => void, {}] 型
const result2 = [...t1, ...t2]; //  (true | 1 | "aaa" | (() => void))[] 型

(Playgroundのリンク貼ろうとしたのですが、URL長すぎてはてなブログの編集画面がフリーズしたので諦めました)

タプルの要素の型が保持されて連結できた
タプルの要素の型が保持されて連結できた

これで連結後のタプルは、ちゃんと0番目はnumberとして推論されますし、3番目は関数として推論されます。

悲しみ

めでたしめでたし…といきたかったのですが、ロジックは正しいのでIDE上では正しい型が表示されてますが、どうやらtscコンパイル時の型のインスタンス化の再帰上限に引っかかってしまうようでコンパイルは出来ませんでした…

型の展開上限に引っかかる
型の展開上限に引っかかる

tscさん型のインスタンス化上限緩和してください!!もしくはコンパイラオプションを!!