🚀

一日一処: TypeScriptで最小値と最大値が決まっている連番のユニオン型を作る

2024/02/17に公開

型の話

TypeScriptでの型の定義は、他の言語と比べ特殊だと感じることが、個人的にはある。ただ、この型の仕組みをよく理解できていれば、複雑な型でも容易に作ることができ、効率のいい、TypeScriptの開発ができるものだと考えている。

最小値と最大値のある連番

様々な状況で、ユニオン型がよく使われる。

type status = 'success' | 'failed'

お馴染みの表現で、TypeScriptを使用している人は、誰しも何も考えず記述できる構文だろう。ただし、このような関数がある場合、そこには、どのような型を当てはめるべきだろうか。

function inputBirthMonth(month) {
  console.log(`your birth month: ${month}`)
}

非常にシンプルな関数だ。ただし、TypeScriptで考えた場合、このmonthにしっかりと型付けしたい。

function inputBirthMonth(month: number) { /* 省略 */}

もちろん、月は数字を求めていることが多いため、このようにすることもあるだろう。
もう少し考えると、正確なのは、こうではないだろうか。

type Month = 1 | 2 | 3 | 4 | 5 | 6
function inputBirthMonth(month: Month) { /* 省略 */}

半年分の数字を一生懸命書いたことは褒めてほしい。半年分で面倒になってしまったが、つまりそういうことだ。たった12個分の数字を書けばいいだけだが、何個も書くものではない。非常に面倒だ。更にそれが、カラーコードやIPアドレスなどのような255まであるような数値だとどうだろうか?西暦もそうだ。これを人の手で書くのは非常に非効率だし、ミスも起きやすい。更に、偶数の数字飲みしたい場合は、絶対に自分の手で書きたくはないだろう。
そんな連番の数字のユニオン型があれば、手放しで喜べるのではないだろうか。

連番のユニオン型

実は、この内容について、日本語で調べたが、同じような記事が全く見当たらなかった。そのため、今回は、誰かのためになるかと思い、日本語の記事にしてみた。このコードの記載元は、Stackoverflowの投稿だ。

type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
  ? Acc[number]
  : Enumerate<N, [...Acc, Acc['length']]>

type NumberRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>

function inputBirthMonth(month: NumberRange<1, 13>) {
  console.log(`your birth month: ${month}`)
}

非常に面白い。これは、様々な仕組みを用いているが、わかりやすいのは、Enumerateで再帰的に実行しているところだろうか。さらに、再帰的に繰り返す判断をしているAcc['length'] extends Nについても面白い。これは、渡された数字と配列の長さが一致しているかを判定している。つまり、この型の内部では、与えられた数字になるまで、配列に配列自身の長さを組み込んでいき、最終的に、配列の長さと与えられた数字が一致した場合、配列の中身をすべて返却する。手法としては、過去に書いた記事の配列からユニオン型を作り出すものと同じだ。
パッと見たときに、このEnumerate型の仕組みに困惑する人もいると思うので、JavaScriptで同等の関数を三項演算子は用いず作った。これ自体は、よくあるタイプのパターンだ。少しは型で何が起きているか理解しやすくなっただろう。

function Enumerate(N, Acc = []) {
  if (Acc.length === N) {
    return Acc
  } else {
    return Enumerate(N, [...Acc, Acc.length])
  }
}

console.log(Enumerate(13))
// [ 0, 1, 2, 3, 4, 5,  6, 7, 8, 9, 10, 11, 13 ]

最後に中間にある型について書く。

type NumberRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>

TとFの渡された型が定義の中で逆になっていることに気づいただろうか。このEnumerateは、与えられた数字までのユニオン型を作るが、配列の構造を使っているがために、常に0から始まる。つまり、下限の数字を0より引き上げたい場合は、特定の数字まで削除する必要がある。そこで登場するのが、Exludeだ。これは、ある型の中身から別の型で重複している項目を削除してくれるものだ。左側に渡されたEnumerate<T>の結果からEnumerate<F>を除去するという具合だ。つまり、0から上限(T)までのユニオン型から0から下限(F)までのユニオン型を取り除くという寸法だ。よくできている。

これで、頭を悩ませずに連番の値を待ち構えることができそうだ。

Discussion