ハードルを下げ続けるブログ@task

常に低い意識で投稿していきたいエンジニアのブログ。当ブログの内容は、個人の見解であり所属する組織の公式見解ではありませんよ。

【日本語訳】Announcing TypeScript 4.0 (2) [WIP]

Announcing TypeScript 4.0 日本語訳のその2です。

やっと本編です。

devblogs.microsoft.com

何が新しいのか?

これらすべてが、4.0 を導いたのです!それでは、さっそく何が新しいのか詳しく見ていきましょう。

Variadic Tuple Types

配列かタプルの2つの引数を取り、それらを連結して新しい配列をつくる JavaScript の関数 concat を検討してみましょう。

function concat(arr1, arr2) {
    return [...arr1, ...arr2];
}

また、配列かタプルを取り、一番目以外の要素すべてを返す tail 関数も考えてみましょう。

function tail(arg) {
    const [_, ...result] = arg;
    return result
}

これらのどちらか TypeScript で型付けするとどのようになるでしょうか?

concat の場合、過去の言語バージョンでできることで唯一有効なのは、いくつかのオーバーロードを書いて試してみることです。

function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

あーっと、これは、、、2つ目の配列が空だったときが、7個のオーバーロードだったということです。 arr2 が一つの要素を持つ場合を足してみましょう。

function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

きりが無いのがお分かりいただけると思います。 不幸にも、tail のような関数でも同じタイプの問題に出くわすでしょう。

これは私達が、「1000のオーバーロードによる死」とよんでいる別のケースで、一般的に問題を解決することすらできません。それは、ただ書く必要のあるオーバーロードの数だけ正しい方を用意するだけです。もしキャッチオールケースを作りたいなら、以下のようなオーバーロードが必要になります。

function concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;

ですが、このシグネチャは入力の長さや、タプルを使用する際の要素の順序については何もエンコードしていません。

TypeScript 4.0 は、型推論の改善に伴って2つの重要な変更がなされてこれらの型付けが可能になりました。

1つ目の変更は、タプル型構文のスプレッドがジェネリックになったことです。これは、扱う実際の型を知らない時でも、タプルと配列での高次演算を表現できるようになったことを意味します。ジェネリックスプレッドが、これらのタプル型でインスタンス化された時(もしくは、実際の型で置き換えられた時)、それらは配列やタプルの型の他のセットを生成できます。

例えば、「1000のオーバーロードによる死」に陥らず、tail のような関数に型付けできるとこを意味しています。

function tail<T extends any[]>(arr: readonly [any, ...T]) {
    const [_ignored, ...rest] = arr;
    return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, 4, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);