TypeScript 4.0で導入されるVariadic Tuple Typesをさっそく使いこなす

今日、Anders Hejlsbergさんによって[TypeScript 4.0に導入予定の新機能のプルリクエストが出されました](https://github.com/microsoft/TypeScript/pull/39094)。この記事では、この新機能**Variadic Tuple Types**を解説します。

ちなみに、現在（この記事の執筆時）のTypeScriptのバージョンは3.9であり、4.0はその次のバージョンです。一見メジャーバージョンアップに見えますが、TypeScriptはsemantic versioningを採用していないため、3.9 → 4.0は特別なリリースというわけではなく、3.8 → 3.9とかと規模は同程度です。

## Variadic Tuple Typesの基本

さて、**Variadic Tuple Types**は、一言でいえば**タプル型の中に`...T`と書ける機能**です。この構文はよく知られたスプレッド構文のいわば型バージョンであり、あるタプル型の要素たちを別のタプル型に埋め込むことができます。次の例では、この機能を`Repeat2`型の定義で使っています。

```ts
type Repeat2<T extends readonly any[]> = [...T, ...T];

// type SNSN = [string, number, string, number]
type SNSN = Repeat2<[string, number]>;
// type BSNSNB = [boolean, string, number, string, number, boolean]
type BSNSNB = [boolean, ...SNSN, boolean]
```

ここで定義されている`Repeat2`は、与えられたタプル型（または配列型）の中身を2回繰り返した型を得られる型です。これを使って作られた`SNSN`型は、`[string, number]`を2回繰り返した型なので`[string, number, string, number]`です。さらに、`BSNSNB`は`SNSN`の両端を`boolean`で囲んだタプル型として定義されており、これは`[boolean, string, number, string, number, boolean]`に解決されます。

### Variadic Tuple Typesのすごい点

Variadic Tuple Typesの特にすごい点は、**型変数に対して`...`を使用できる**点です。上の例では`Repeat2`がこれをやっています。`...`で展開される型は、`extends readonly any[]`という条件（またはより厳しい条件）を満たす必要があります。この条件は、任意の配列型またはタプル型が満たす条件です。

特に、**型変数を型変数のまま扱える**点が画期的です。これが意味することは、タプル型の中で`...T`として使われている型変数`T`が**推論の対象になる**ということです。

Variadic Tuple Typesでは、このような記法を導入したことに加え、型推論なども強化されて`...T`に対してしっかり推論が働くようになります。この記事の後半で見ますが、このことがVariadic Tuple Typesを強力な道具にしています。

### 従来の可変長タプルとの違い

ところで、TypeScriptには従来から**可変長タプル型**という機能がありました。例えば次のような型を宣言することは従来から可能だったのです。

```ts
type NNSs = [number, number, ...string[]];
```

これは「最初が`number`、次も`number`、残りが0個以上の`string`」という条件を満たす配列型です。

こう見ると、昔から`...`が使えたように思えますが、従来は2つの制限がありました。

- `...`が使えるのは最後に1回のみ。
- 必ず`...X[]`の形で使う必要がある。

つまり、従来は「残りが0個以上の`X`」ということしか表現できなかったのです。最後以外で`...`を使うのはエラーとなりますし、最後でも次のような場合は`...X[]`の形になっていないのでエラーとなります。

```ts
// TypeScript 3.9以下ではエラー

// エラー :A rest element type must be an array type.
type NTs<T extends readonly any[]> = [number, ...T];
```

たとえ`T`が配列であることがわかっていたとしても、従来は`...T`は許されませんでした。これは`...X[]`の形ではないからです。

まあ、`...T`が許されると`T`が配列ではなくタプル型（固定長）かもしれず、`[number, ...T]`が必ず可変長になるとは限らないという新たな可能性が発生してしまうので、従来は必ず可変長になる形でしか使えないように制限されていたと見ることができます。

### Variadic Tuple Typesでできないこと

逆に、TypeScript 4.0でVariadic Tuple Typesが導入されてもできないことはあります。具体的には、次のような型は定義できません。

```ts
// エラー: A rest element must be last in a tuple type.
type SsNs = [...string[], ...number[]];
```

型`SsNs`の意図は「前半に`string`が並んでいて後半に`number`が並んでいる配列」というものですね。上の3つは「文字列→数値」という順番なのでOKで、残り2つはそうなっていません。

```ts
// ご注意：これは動きません！
const valid1: SsNs = ["foo", "bar", "baz", 0, 1, 2];
const valid2: SsNs = ["foo", 0, 1, 2];
const valid3: SsNs = [0];
const invalid1: SsNs = [0, 1, 2, "foo"];
const invalid2: SsNs = ["foo", 0, "bar", 1];
```

ところが、**このような型定義はできません**。`SsNs`の型定義の時点でコンパイルエラーとなります。

整理すると、Variadic Tuple Typesが導入されても、**長さが可変長の部分は最後だけ**という制限は残ります。上の`SsNs`は、`...string[]`という可変長部分を最後以外の部分に置こうとしているため、TypeScriptのサポート外です。同様に、`[...string[], number]`のようなものもサポートされません。

しかし、ここにひとつ厄介な問題があります。先ほどVariadic Tuple Typesの`...T`で展開できる`T`の条件は`readonly any[]`の部分型であることと述べました。つまり、`T`は固定長のタプル型とは限らず`string[]`のような配列型かもしれないということです。ということは、次のように`NSsN`型を定義してもコンパイルエラーにはなってくれません。

```ts
type AddN<T extends readonly any[]> = [number, ...T, number];

// type NSBN = [number, string, boolean, number]
type NSBN = AddN<[string, boolean]>;

// type NSsN = [number, ...(string | number)[]]
type NSsN = AddN<string[]>;
```

この例の`AddN`は、与えられたタプル型`T`の最初と最後に`number`を足した新しいタプル型を得るための型です。想定された使い方は、`NSBN`のように固定長のタプル型をいじるものです。

しかし、この定義だと`NSsN`の定義のように、この`AddN`に`string[]`のようなものを渡すことができます。現状、型変数に対して「固定長のタプル型のみ」のような制限を課す方法がないため、このような物は型エラーにはできないのです。

このような場合は、可変長の部分以降が全部吸収されます。具体例は上の`NSsN`で、これは`[number, ...(string | number)[]]`となります。本来意図していたものは`[number, ...string[], number]`ですが、このようなものはTypeScriptでサポートされませんので、解決の際に`...string[]`の後ろの`number`が`...string[]`に吸収されて`...(string | number)[]`となります。

この“吸収”という動作はPRの説明にも記載されており、バグではなく仕様通りの動作です。意図と異なる結果となるため戸惑う方が多そうですが、前述のようにTypeScriptではタプル型の最後以外の位置での可変長要素を（少なくとも今の段階では）サポートしませんから、やむを得ない挙動であると考えられます。

まとめると、タプル型の最後以外に可変長の要素（`...string[]`のようなもの）がある型はTypeScriptでは今のところ表現できません。構文上明らかな場合（`[...string[], ...number[]`）は宣言の段階でコンパイルエラーとして弾かれますが、型変数が配列型（可変長）かタプル型（固定長）か分からない場合には必ずしも弾くことができません。そのような場合は“吸収”の挙動により最後が可変長のタプル型に矯正されます。

## 関数宣言でVariadic Tuple Typesを使う

さて、Variadic Tuple Typesは`...`で（特定の条件を満たす）型変数も展開できるのが良い点です。これで特に嬉しいのは、**関数宣言（特にジェネリクス）と一緒に使える**ことです。

たとえば、こんな関数を作ることができます。

```ts
function repeat<T extends readonly any[]>(arr: [...T]): [...T, ...T] {
    return [...arr, ...arr];
}

// const t: [string, number, string, number]
const t = repeat(["foo", 1]);
```

ここで定義された関数`repeat`は、与えられた配列を2回繰り返した配列を返す関数です。Variadic Tuple Typesによって、この関数には`<T extends readonly any[]>(arr: [...T]) => [...T, ...T]`という型を付けることができます。

ちなみに、引数の`[...T]`は今回新しく追加されたテクニックで、**`T`として配列型ではなくタプル型を推論してほしい**ときに使えます。意味的には`[...T]`はただの`T`と同じですが、型引数の推論の際の挙動が異なるのです。

今回`repeat(["foo", 1])`のように`repeat`を呼び出すと、引数`["foo", 1]`に`[...T]`が当てはめられるため、型引数`T`は`[string, number]`として推論されます。よって返り値の型が`[...T, ...T]`なので、変数`t`は`[string, number, string, number]`型と推論されます。

型引数の推論もさることながら、配列リテラル中のスプレッド構文の型推論に対しても拡張が加えられています。具体的には、`[...T]`型の変数`arr`に対して、`[...arr, ...arr]`という式の型は`[...T, ...T]`であることの推論ができていますね。

他の例としては、次のような`tail`関数も定義できます。

```ts
function tail<S, T extends readonly any[]>(tuple: readonly [S, ...T]): T {
    const [, ...rest] = tuple;
    return rest;
}

// const t2: [number, string, number]
const t2 = tail(["foo", 1, "bar", 2]);
```

この`tail`関数は、与えられた配列の最初の要素を除いた配列を返します。使用例では`[string, number, string, number]`型の配列を引数に与えたので、返り値の型は`[number, string, number]`となっています。この場合も、与えられた引数から型引数`S`, `T`を推論することに成功しています。具体的には、`["foo", 1, "bar", 2]`が`readonly [S, ...T]`とマッチングされたことによって、`S`が`string`であり`T`が`[number, string, number]`であることを推論しています。

なお、お察しの通り、型がリテラル型ではなく`string`や`number`などに広げられるのが気に入らない場合は`as const`を使って解決できます。

```ts
// const t3: [1, "bar", 2]
const t3 = tail(["foo", 1, "bar", 2] as const);
```

関数`tail`の定義でわざわざ引数のタプル型に`readonly`とつけていたのは、`as const`によって作られる`readonly`タプルを受け入れられるようにするためです。筆者の以前の記事「[TypeScriptのreadonlyプロパティを使いこなす](https://qiita.com/uhyo/items/0fd033ff1aed9b4b32dd)」では以下のように述べましたが、Variadic Tuple Typesの登場によって適切な`readonly`の付与は今後さらに重要になるでしょう。

> `readonlyを使いこなすには、可能なところにできる限り`readonly`を付与します。特に、関数引数が受け取るオブジェクトや配列は、関数内で変更しないならば積極的に`readonly`を付与しましょう。

ちなみに、`tail`の中身に目を向けると、分割代入の型推論に対してもやはり強化が入っていることが伺えます。具体的には、次の箇所で`rest`にちゃんと`[...T]`型が与えられます。

```ts
// const rest: [...T]
const [, ...rest] = tuple;
```

## 可変長引数とVariadic Tuple Types

Variadic Tuple Typesの面白いところは、可変長引数と組み合わせることができる点です。次のようにすると、「可変長引数だが最初と最後の引数は`number`でなければならない」という謎の関数を作ることができます。

```ts
function func<Args extends readonly any[]>(...args: readonly [number, ...Args, number]) {
    const num1 = args[0];
    const num2 = args[args.length - 1] as number;
    console.log(args.slice(1, -1));
    return num1 + num2;
}

func(1, 2, 3, 4, 5);
func(1, "foo", "bar", 2);
// エラー: Argument of type 'string' is not assignable to parameter of type 'number'.
func("foo", 1, 2, 3);
// エラー: Expected 2 arguments, but got 1.
func(1);
```

## Conditional TypesとVariadic Tuple Types

Variadic Tuple Typesは、Conditional Types（特に`infer`）と組み合わせることもできます。たとえば、「タプル型の最初の要素を削ったタプル型を返す型関数」は次のように定義できるでしょう。

```ts
type Tail<T extends readonly [any, ...any[]]> = T extends readonly [any, ...infer U] ? U : never;

// type SB = [string, boolean]
type SB = Tail<[number, string, boolean]>;
```

TypeScript 3.9以前では、`...infer U`が不可能でした（`...X[]`の形ではないため）。そのため、同じことをするには次のように関数型を経由する必要がありました。Variadic Tuple Typesによりタプル型の表現力が向上したことで、より直感的に`Tail`のような型が書けるようになったのは嬉しいですね。

```ts
type Tail<T extends readonly [any, ...any[]]> = ((...args: T) => void) extends (arg0: any, ...args: infer U) => void ? U : never;
```

同様に、タプル型の最初に型を付け加える`Cons`も簡単に定義することができるようになりました。というか、これならもはや`Cons`を定義する必要すらありませんね。

```ts
type Cons<U, T extends readonly any[]> = [U, ...T];

// type SSB = [string, string, number]
type SSB = Cons<string, [string, number]>;
```

## 配列リテラル・スプレッド構文に対する推論の変化

最後に、Variadic Tuple Typesによってもたらされる間接的な推論の変化について述べておきます。冒頭にでてきた`repeat`を少し変えた関数を考えてみます。

```ts
function repeat<T extends readonly any[]>(arr: T) {
    const result = [...arr, ...arr];
    return result;
}
```

これはVariadic Tuple Typesの構文を使っていないので、TS 3.9以前でもコンパイルできる関数です。しかし、型推論の結果はTS 4.0から変化します。

TS 3.9では、変数`result`が`any[]`型に推論されます。よって、`repeat`の返り値の型は`any[]`です。なかなか危険ですね。

一方、TS 4.0では`result`の型は`T[number][]`に推論されます。これは「`T[number]`の配列」であり、`T[number]`とは「`T`の要素の型」です。たとえば`T`が`[string, boolean]`というタプル型ならば、`T[number]`は`string | boolean`になります。よって、`T`が`[string, boolean]`だったとしたら、`repeat`の返り値の型は`(string | boolean)[]`となるわけですね。実際、`result`という配列の要素は全て`arr`の要素（つまり`T`の要素）ですから、この推論は妥当ですね。

このように、Variadic Tuple Typesの導入に伴って配列リテラルのスプレッド構文の推論が強化されています。

ところで、冒頭に開設した流れだと`repeat`の返り値の型はTS 4.0では`[...T, ...T]`となるべきだと思われますが、そうはなっていませんね。実は、`[...T, ...T]`を得るためには配列リテラルに対して`as const`が必要です。次のように変えれば、変数`result`の型は`readonly [...T, ...T]`となります（TS3.9では`readonly any[]`になります）。

```ts
function repeat<T extends readonly any[]>(arr: T) {
    const result = [...arr, ...arr] as const;
    return result;
}
```

このように、タプル型をスプレッドさせる場合でも、配列リテラルに`as const`を用いないと結果がタプル型にならないことは特筆に値します。

## タプル型に対するインデックスアクセスに対する推論

Variadic Tuple Typesにより「型引数を内包するタプル型」が扱えるようになったことから、TS 4.0では与えられた配列の最初の要素を返す関数`head`を次のように書くことができます。

```ts
function head<T extends readonly any[]>(args: readonly [...T]) {
    const result = args[0];
    return result;
}

// const str: "foo"
const str = head(["foo", "bar"] as const);
```

なんと、変数`str`の型を見るとちゃんと`"foo"`が推論されます。実は、この関数`head`の返り値の型を調べると`readonly [...T][0]`となっています（これは`readonly [...T]`の`0`番目の要素という意味です）。

このように、具体的ではない（型変数で表される）型を持つ配列に対して`arr[0]`のようにインデックスアクセスをした場合にLookup Typeが推論されるのが特徴的です。実際に`head`を上記の例のように呼び出すと、`T`が`readonly ["foo", "bar"]`に推論されて、`readonly [...T][0]`が`"foo"`に解決されます。

この推論機構は、`args`の型を`[...T]`のようにタプル型と明示しないと使えません。これを`args: T`とした場合、返り値は`any`型になってしまいます。

ところで、`args[0]`を返すとなると、空の配列`[]`を渡したときの挙動が気になりますよね。つまり、`T`が`[]`だった場合に`readonly [...T][0]`は何になるのかということです。早速やってみましょう。

```ts
// const u: undefined
const u = head([])
```

なんと、`head([])`の返り値は`undefined`型になりました。実際、空の配列にに対して`0`番目の要素を取得すると`undefined`が返りますから、これは妥当な型推論です。天才ですね。

## まとめ

Anders Hejlsbergさんの天才的な成果により、TypeScript 4.0からはVariadic Tuple Typesが使えるようになる見込みです。

Variadic Tuple Typesの最大の特徴は**タプル型の中に`...T`のようなスプレッド構文を混ぜることができる**点であり、また**具体化される前の型変数が混ざったタプル型をうまく扱えるようになる**のもとても重要です。

今後我々に要求されるテクニックは、`T`としてタプル型が求められる場合ただ`T`と書くのではなく`readonly [...T]`と書くことです。こうすることによって、Variadic Tuple Typesの機能を最大限活かすことができるでしょう。

なお、この記事を公開した時点でVariadic Tuple TypesのPRはまだマージされていませんので、いますぐ試したい方は自分でTypeScriptコンパイラをビルドする必要があります。この記事の内容は、当該PRの現時点の最新コミットである`759352206d6c411e03ffb096a14bc9deab841dac`に基づいています。