TypeScriptで最低n個の要素を持った配列の型を宣言する方法

TypeScriptでは配列の型は`T[]`のように宣言します（`Array<T>`でも可）。この配列は、もちろん要素数が何個でも構いません。

```ts
const arr1: number[] = [0, 1, 2];
const arr2: number[] = [0];
const arr3: number[] = [];
```

しかし時折、「2個以上の要素を持った配列」のような条件を書きたくなることがあるかもしれません。すなわち、**配列の最低要素数を型で指定したい**という場合ですね。実はTypeScriptでは、タプル型を応用することでこれが可能です。

## タプル型を用いた最低要素数の表現

```ts
// 要素が最低2個あるT型の配列
type AtLeast2<T> = [T, T, ...T[]];

const arr1: AtLeast2<number> = [0, 1, 2]; // これはOK
const arr2: AtLeast2<number> = [0, 1];    // これもOK
const arr3: AtLeast2<number> = [0];       // これはエラー
```

ここで宣言されている`AtLeast2<T>`型の中身は`[T, T, ...T[]]`という型です。このように`[]`の中に型を書き連ねたものを**タプル型**といいます（詳しくは筆者の過去記事[TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a#%E3%82%BF%E3%83%97%E3%83%AB%E5%9E%8B)をご参照ください）。これの意味を簡単にいうと、「最初の要素は`T`、次の要素も`T`、そして残りの部分が`T[]`に当てはまるような配列の型」です。宣言の時点で「最初の要素」と「次の要素」の存在が確定しているため、これは最低2個の要素を持つ`T`型の配列という意味になるのです。

なお、要素の型は全部`T`に統一する必要はありません。例えば`[string, string, ...number[]]`だと、「最初の2つの要素が文字列で残りの要素は数値」という条件の配列の型を宣言できます。

## 一般化

さて、ここからが本題です。上の例では最低要素数は「2個」と決まっていました。他にも1個とか3個、5個の場合が欲しいときはどうすればいいでしょうか。一つの手はこうすることです。

```ts
type AtLeast1<T> = [T, ...T[]];
type AtLeast3<T> = [T, T, T, ...T[]];
type AtLeast5<T> = [T, T, T, T, T, ...T[]];
```

しかしこれでは美しくないですね。理想的なのは、`AtLeast<N, T>`という型をひとつだけ定義して`AtLeast<1, T>`とか`AtLeast<3, T>`や`AtLeast<5, T>`のように使うことです。

そこで、この記事ではこれを実現する方法を紹介します。

### 結論

結論からいうとこうすればできます。

```ts
type Append<Elm, T extends unknown[]> = ((
  arg: Elm,
  ...rest: T
) => void) extends ((...args: infer T2) => void)
  ? T2
  : never;

type AtLeast<N extends number, T> = AtLeastRec<N, T, T[], []>;

type AtLeastRec<Num, Elm, T extends unknown[], C extends unknown[]> = {
  0: T;
  1: AtLeastRec<Num, Elm, Append<Elm, T>, Append<unknown, C>>;
}[C extends { length: Num } ? 0 : 1];

// ---------- 使用例 ----------
const arr: AtLeast<3, number> = [0, 1];      // これはエラー
const arr2: AtLeast<3, number> = [0, 1, 2];  // これはOK

const arr3: AtLeast<5, number> = [0, 1, 2];  // これはエラー
const arr4: AtLeast<5, number> = [0, 1, 2, 3, 4]; // これならOK
```

使用例を見ると、やりたいことがちゃんと実現できていることが分かります。また、例えば`arr2`の型を調べると`[number, number, number, ...number[]]`となっており、先ほどと原理は同じであることが分かります。

[![Screenshot from Gyazo](https://gyazo.com/e954515d28b2ae8695c7d00970a77ecb/raw)](https://gyazo.com/e954515d28b2ae8695c7d00970a77ecb)

ちなみに、`AtLeast`の型引数に`3`や`5`といった数値を渡しているように見えますが、これは**数値のリテラル型**です。詳しくは前述の「型入門」を読んでほしいのですが、例えば`3`型は`3`という数値のみが許される型です。

### 解説

では上記の例を解説していきます。まず、`AtLeast<3, number>`がどのように評価されるのかを確かめます。

定義に従って、`AtLeast<3, number>`は`AtLeastRec<3, number, number[], []>`になります。

詳細はあとで戻ってきて説明しますが、`AtLeastRec`の定義に従ってこれを展開すると`AtLeastRec<3, number, [number, ...number[]], [unknown]>`になります。

再び`AtLeastRec`の定義に従って展開すると`AtLeastRec<3, number, [number, number, ...number[]], [unknown, unknown]>`になります。

さらに`AtLeastRec`の定義に従って展開すると`AtLeastRec<3, number, [number, number, number, ...number[]], [unknown, unknown, unknown]>`になります。

そして、これを`AtLeastRec`の定義に従って展開すると`[number, number, number, ...number[]]`になります。

このように、`AtLeastRec<Num, Elm, T, C>`は**型レベル再帰**を行いながら第三引数`T`を育てていきます。`T`は最初`Elm[]`、つまり要素数0個以上の配列です。1回再帰するごとに`T`の最初に要素`Elm`が追加され、`T`の最低要素数が1つ増えます。よって、`Num`回再帰してから`T`を返すことによって、最低要素数`Num`の`T`の配列を作っているのです。

この再帰を制御するには、「**今は何回目の再帰か**」という情報を保持する必要があります。さっき何気なく登場した数値のリテラル型を使えばいけるように思えますが、それはできません。なぜなら、数値のリテラル型は数値計算をサポートしていないからです。「`3`というリテラル型に1を足して`4`というリテラル型にする」というような型レベル計算はできません。

そこで、今回は**タプル型を用いて自然数を表しています**。それが`AtLeastRec`の第4型引数`C`の役割です。実は、**TypeScriptではタプル型の要素数を`length`プロパティを通じて数値のリテラル型の形で得ることができます**。つまり、`[]`（0要素のタプル型）の`length`プロパティは`0`型だし、`[unknown, unknown]`（2要素のタプル型）の`length`プロパティは`2`型を持っています。

先ほどの再帰で`AtLeastRec`の第4型引数`C`に注目すると、最初は`[]`となっています。これは再帰が0回目である（最初の再帰である）ことを表しています。次の再帰では`C`は`[unknown]`です。またその次では`[unknown, unknown]`となっています。このように1回の再帰につき`C`の先頭に`unknown`を足すことによって、`C`を再帰の回数のカウンタとして用いているのです。`AtLeastRec`は`C`の要素数を見てそれが`Num`になっていたら、`Num`回再帰したと見なして再帰を終了し結果を返します。

### 素朴な実装

以上で解説したアイデアを素朴に書き下してみると、こんな感じになるはずです。

```ts
type AtLeast<N extends number, T> = AtLeastRec<N, T, T[], []>;

type AtLeastRec<Num, Elm, T extends unknown[], C extends unknown[]> =
  C extends { length: Num }
    ? T
    : AtLeastRec<Num, Elm, [Elm, ...T], [unknown, ...C]>;
```

`AtLeastRec`は、今が`Num`回目の再帰かどうかを条件型 (conditional type) を用いて判定します。`C extends { length: Num }`という条件は`C`の`length`プロパティが`Num`であるという条件を表しており、`C`がタプル型であるという前提を踏まえると`C`の要素数がちょうど`Num`個であるという意味になります。

その場合は、これまで育ててきた`T`を結果として返します。そうでなければ`T`の先頭に`Elm`を足したタプル型`[Elm, ...T[]]`を作り新たな`T`とします。また、`C`にも`unknown`をひとつ足して`[unknown, ...C[]]`とします。

お分かりの通り、`C`に足す`unknown`は特に意味はなく、なんでも構いません。

しかし、上の素朴な実装には2つの問題があります。ひとつは`[Elm, ...T]`とは書けない（タプル型の末尾の`...`で型引数を展開することはできない）という点、そしてもうひとつは**TypeScriptでは型の再帰を直接書けない**という点です。

ここからは、この2つの問題点を回避する方法を説明します。

### TypeScriptでタプル型の先頭に要素を追加する方法

TypeScriptでは、たとえ`T`が配列型であることが明らかでも`[Elm, ...T]`と書くことは許可されていません（この制約がなぜ存在するのかはよく分かりません。実装をサボっているだけかもしれませんが）。

この制約は、**関数型を経由する**ことで回避できます。TypeScriptではタプル型と（可変長引数）関数の型は相互変換可能であり、しかも関数の側では「先頭に要素を追加する」という操作が可能なのです。

タプル型から関数型への変換は簡単です。可変長引数の構文を用いてタプル型を展開します。次の例が分かりやすいでしょう。`...args`のところにタプル型を付けることで、タプル型が引数の型に展開されています。

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

// F1 は (args_0: number, args_1: string, ...args_2: number[]) => void という型になる
type F1 = (...args: T1) => void;
```

逆に、関数型からタプル型への変換は条件型を使います。

```ts
type F2 = (arg0: number, arg1: string, ...args: number[]) => void;

// T2 は [number, string, ...number[]] 型
type T2 = F2 extends (...args: infer T) => void ? T : never;
```

この例では条件型を型レベルパターンマッチのように使用しています。`F2`という関数の型を`(...args: infer T) => void`にマッチさせることで、型引数`T`に引数の型一覧をタプル型の形で入手しているのです。条件型における`infer`キーワードは、このように型引数を導入してパターンマッチの結果を取り出すのに利用できます。

この場合`F2`は必ずマッチするので、マッチしなかった場合の返り値の型`never`に特に意味はありません。

このようにしてタプル型と関数型の相互変換ができました。そして、実は関数型では可変長引数より前に引数を追加することが可能なのです。

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

// F3 は (firstArg: boolean, args_0: number, args_1: string, ...args_2: number[]) => void という型になる
type F3 = (firstArg: boolean, ...args: T3) => void;
```

この例では、タプル型から関数型に変換するときに引数を先頭にひとつ追加しています。`[boolean, ...T3]`という型はエラーにされてしまいましたが、関数型では同じことが許されるのです。

ということで、タプル型の先頭に要素を追加したい場合は一旦関数型にして追加してからタプル型に戻せばいいことが分かりました。この部分を型関数として切り出すとこうなります。

```ts
type Append<Elm, T extends unknown[]> = ((
  arg: Elm,
  ...rest: T
) => void) extends ((...args: infer T2) => void)
  ? T2
  : never;

// ---------- 使用例 ----------
// T1 は [number, string, string] 型
type T1 = Append<number, [string, string]>;
// T2 は [number, string, ...unknown[]] 型
type T2 = Append<number, [string, ...unknown[]]>;
```

早速これを使って`AtLeastRec`を書き換えましょう。

```ts
type Append<Elm, T extends unknown[]> = ((
  arg: Elm,
  ...rest: T
) => void) extends ((...args: infer T2) => void)
  ? T2
  : never;

type AtLeast<N extends number, T> = AtLeastRec<N, T, T[], []>;

type AtLeastRec<Num, Elm, T extends unknown[], C extends unknown[]> =
  C extends { length: Num }
    ? T
    : AtLeastRec<Num, Elm, Append<Elm, T>, Append<unknown, C>>;
```

これでエラーがひとつ解消できました。

### TypeScriptで型の再帰を書く方法

もうひとつの問題は**TypeScriptでは型の再帰を直接書けない**という点でした。上の例では`AtLeastRec`が再帰になっていることがTypeScriptコンパイラにバレてしまい、怒られてしまいます。

そのため、型の再帰を書くにはひと工夫する必要があります。いろいろな手法がありますが、簡単なのは**再帰部分をオブジェクト型に包み、さらに条件型を用いて遅延評価しながら取り出す**というものです。

何を言っているのか分からないと思いますので、先に結論を見ましょう。`AtLeastRec`をこう書き換えます。

```ts
type AtLeastRec<Num, Elm, T extends unknown[], C extends unknown[]> = {
  0: T;
  1: AtLeastRec<Num, Elm, Append<Elm, T>, Append<unknown, C>>;
}[C extends { length: Num } ? 0 : 1];
```

まず、再帰部分`AtLeastRec<Num, Elm, Append<Elm, T>, Append<unknown, C>>`が`{ 0: T; 1: /* 省略 */ }`という形でオブジェクト型の中に入りました。これが**オブジェクト型で包む**ということです。

このようにオブジェクト型で包むことによって、型が再帰していても弾かれなくなります。ここで、このオブジェクト型は`0`と`1`という2つのプロパティを持っています。`0`は`C extends { length: Num }`が満たされたときに返したい型、`1`はそうでないときに返したい型が入っています。

とはいえ、オブジェクト型から（プロパティアクセス型`T[K]`を用いて）中身の型を取り出すときに`{ /* 省略 */ }[1]`のようにやってしまうと、オブジェクト型が解決されてしまうため結局直接再帰している扱いとなり、やはり同様のエラーとなります。

ということで、ここでもうひとつのポイントである**条件型を用いて遅延評価しながら取り出す**ということを行っています。具体的には、型を取り出すときのキー（プロパティ名）を`C extends { length: Num} ? 0 : 1`という条件型で決定しています。

条件型には遅延評価されるという性質があるため、この型の宣言を見ただけでは`0`と`1`のどちらが返されるのか分かりません。これにより、宣言の段階でプロパティアクセス型が解決されるのを回避していま。

これも何だかTypeScriptの推論器の穴を突いている気がして、そのうち塞がれそうでちょっと怖いですね。しかしとにかくこれでエラーを回避しながら目的を達成できました。

## まとめ

この記事では、タプル型を用いて最低要素数が決まっている配列の型を宣言するという手法を紹介し、さらにその最低要素数を数値リテラル型を用いて指定できるように一般化する方法を紹介しました。

ポイントは**型レベル再帰の扱い方**や**タプル型と配列型の相互変換**といったテクニック、そして**タプル型をカウンタとして扱う**というアイデアです。知識とアイデアがあればこれくらいのことができるというTypeScriptの面白さを知っていただけると幸いです。

ちなみに、型レベル再帰の回数には制限があるので注意してください。今回の場合だと、`AtLeast<46, number>`のように要素数を46以上にすると再帰の限界を超えてエラーになりました。

### 謝辞

この記事は以下のツイートから着想を得ました。この場を借りてお礼申し上げます。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">TypeScript で配列の要素数を制限する方法があるの初めて知った．例えばこれは要素数2以上の配列：<br><br>type A&lt;T&gt; = [T, T, ...T[]]<br><br>const a1: A&lt;number&gt; = [1, 2, 3]; // OK<br>const a2: A&lt;number&gt; = [1, 2]; // OK<br>const a3: A&lt;number&gt; = [1]; // ERROR</p>&mdash; ドッグ (@Linda_pp) <a href="https://twitter.com/Linda_pp/status/1135199928462954497?ref_src=twsrc%5Etfw">2019年6月2日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">それみると、 A&lt;number, _10&gt; みたいに、要素数をリテラルっぽく書けるといいなって気がしました。ただ、TypeScriptの型レベル計算だと難しいですかね…。</p>&mdash; Kota Mizushima (@kmizu) <a href="https://twitter.com/kmizu/status/1135893303130386432?ref_src=twsrc%5Etfw">2019年6月4日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>