TypeScriptで特定の範囲の整数だけを取り得る型を作りたいことがあります。
0~3までのように短い場合は以下のように列挙して書くことが多いです。
type ShortRange = 0 | 1 | 2;
しかし、0~100のようにある程度長い範囲を取る場合は列挙させて書くのは大変面倒です。以下のように便利な型を作れば手間を省略できます。
type Range<
N extends number,
Result extends number = never,
C extends never[] = [],
> = C['length'] extends N
? Result
: Range<N, Result | C['length'], [...C, never]>;
このRange型はRange<3>とすると0 | 1 | 2、Range<101>とすると0 | 1 | ... | 100のような型を生成します。
この型はCを目安に指定した数値まで再帰的に呼び出して目的の型を取り出します。
型引数のNは上限の数値、Resultは最後に返す型で初期値はneverにしています(つまりRange<0>はneverです。)。Cは再帰数の目安に使う型で初期値が[]のnever型の配列です。再帰数は配列の長さC['length']を元に求めます。
C['length'] extends Nは指定した数値Nに達しているかどうかをチェックします。
達していなければRangeを再度呼び出します。
第1型引数は据え置きなのでその時点のNを次に託します。
第2型引数は最終的に返す型です。その時点のResultとCの長さをユニオンで渡します。
第2型引数は再帰数を表す配列です。Cに新たにneverを詰め込んで更新します。
C['length']とNが一致すればこれまで作り上げてきたResultを返します。
この型は再帰を用いているので上限があることに気をつけてください。突破すると以下のようなエラーが出ます。
Type instantiation is excessively deep and possibly infinite.
私の環境ではRange<1000>が上限でした。
Range<3>だと以下のような流れです。
Rangeの呼び出し数 |
N |
Result |
C |
|---|---|---|---|
| 1回目 | 3 |
never |
[] |
| 2回目 | 3 |
0 |
[never] |
| 3回目 | 3 |
0 | 1 |
[never, never] |
| 4回目 | 3 |
0 | 1 | 2 |
[never, never, never] |
neverは何かとユニオンを取ると消滅するのでnever | 0ではなく0と表記しています。
おまけ1
Range<N>で指定したNも含めたい場合は以下のようにします。
type Range<
N extends number,
Result extends number = N,
C extends never[] = [],
> = C['length'] extends N
? Result
: Range<N, Result | C['length'], [...C, never]>;
Resultの初期値をNにしておくことで、最初に紹介した型の結果とNのユニオンが作られるようにしました。
他にも最終的な型をいじって表現するなどいくつかの方法で書けます。
type Range<
N extends number,
Result extends number = never,
C extends never[] = [],
> = C['length'] extends N
? Result | C['length']
: Range<N, Result | C['length'], [...C, never]>;
おまけ2
n~mの整数のように両方の値を指定する場合は以下のようにします。
type Range<
N extends number,
M extends number,
Result extends number = never,
C extends never[] = [],
Flag extends boolean = false,
> = C['length'] extends N
? Range<N, M, Result | C['length'], [...C, never], true>
: Flag extends true
? C['length'] extends M
? Result
: Range<N, M, Result | C['length'], [...C, never], Flag>
: Range<N, M, Result, [...C, never], Flag>;
新たにMとFlagを型引数に取るようにしました。Nが範囲の最初の数値、Mは範囲の終端です。
これも同じくCで管理する再帰数を元に型を作って最終的にC['length']がMとぶつかった時のResultを返します。
異なる点はC['length']がNとぶつかるまではResultを更新しないところです。衝突するまではFlagはfalseを取りその間はResultを更新せずに再帰数だけ更新します。衝突したタイミングからFlagをtrueに切り替えてResultの更新を開始するようにしました。