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
の更新を開始するようにしました。