TypeScriptで整数値の範囲を表現するUnion型をつくるUtility Typeを定義する

TypeScriptである任意の範囲を表現する整数型を定義することを考えます。

たとえば、1~12の範囲を表現する場合、以下のように書くことができます。

type MonthRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12

これを以下のように書けると便利です。

type MonthRange = NumberRange<1, 12>

このときの NumberRange 型を定義してみます。

話を簡単にするため、まずは 0 から任意の数までの連続した整数型を持つUnion型をつくる型を定義してみます。

type NumberRange<
  U extends number,                                             // NumberRangeArray型に渡す to
  Z extends true[] = [],                                        // 繰り返した回数を記録する配列型(初期は空の配列型)
  W = 0                                                         // NumberRangeを再帰的に呼ぶので
                                                                // 作成したUnion型の途中経過をW型に格納する
> = Z['length'] extends U ? W : NumberRange<U, [...Z, true], W | Z['length']>

U型と配列型Zの長さが一致するまで、W型にその時点での配列型Zの長さを整数型として追加していくという型定義です。 NumberRangeは再帰的に呼び出され、呼び出すたびに配列型Zに要素の型を追加していきます(配列の長さが変わればいいので、適当に true 型の要素を追加しています)。配列型Zの長さとUが一致するまで繰り返されます。

このとき、例えば以下のようにした場合、

type Month = NumberRange<12>

Month 型は 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 型になります。(TypeScript Playgroundで確認する)

NumberRange 型を発展させ、任意の数から任意の数までの連続した整数値を持つUnion型をつくる型に書き換えます。

type ArrayFixedLength<T extends number, U, V extends Array<U> = [], W extends Array<true> = [true]> = 
    W[T] extends true ? V : ArrayFixedLength<T, U, [...V, U], [...W, true]> 

type NumberRange<
  T extends number,                                             // NumberRangeArray型に渡す from
  U extends number,                                             // NumberRangeArray型に渡す to
  Z extends true[] = ArrayFixedLength<T, true>,                 // 繰り返した回数を記録する配列型
                                                                // 予め数列の始点分の配列型をセットしておく
  W = T                                                         // NumberRangeを再帰的に呼ぶので
                                                                // 作成したUnion型の途中経過をW型に格納する
> = Z['length'] extends U ? W : NumberRange<T, U, [...Z, true], W | Z['length']>

(上記ソースコードにある ArrayFixedLength<T, U> 型は、以前作った「任意の要素を持つ配列型」を生成する型定義です。Tに指定した整数値分のU型の配列をつくります。)

NumberRange 型のうち、配列型Zにはあらかじめ初期型として、長さTぶんの配列をセットしておくと、そのぶんNumberRangeを再帰的に呼び出す回数が減ります。かつW型には初期値として数列の開始値Tをセットしておき、連続する整数値の開始時の値としています。

type MonthRange = NumberRange<1, 12>

としたとき、 MonthRange1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 型となります。

この型定義では、配列型Zの長さはU型と一致すると再帰呼び出しを終了するので、型「12」は含まれていません。(TypeScript Playgroundで確認する)

これだと使いづらいので、さらに改善していきます。

type NumberRange<
  T extends number,                                             // NumberRangeArray型に渡す from
  U extends number,                                             // NumberRangeArray型に渡す to
  Y extends unknown[] = [...ArrayFixedLength<U, true>, true],   // 終了条件を判定する型
  Z extends true[] = ArrayFixedLength<T, true>,                 // 繰り返した回数を記録する配列型
  W = T                                                         // NumberRangeを再帰的に呼ぶので
                                                                // 作成したUnion型の途中経過をW型に格納する
> = Z['length'] extends Y['length'] ? W : NumberRange<T, U, Y, [...Z, true], W | Z['length']>

Y型は、長さ(U+1)の配列型です。配列型Yの長さと配列型Zの長さを比較するように修正することで、先ほどの終了条件(Uと配列型Zの長さ)よりも1回多く処理を実行できます。(TypeScript Playgroundで確認する)

type MonthRange = NumberRangeArray<1, 12>

としたとき、MonthRange 型は 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 型になり、期待した結果が得られました。

たのしい型パズルの様子をお届けしました。ではでは~

このカウンタは @piyoppi/counter-tools を使っています。

クリックすると匿名でいいねできます。