Posted at

TypeScriptでn未満の自然数を表す union 型を作る

JavaScript 標準の Math.random は小数を返しますが、「指定した n 未満の自然数」が欲しい場合があります。というかそれが便利です。

const a = randomInt(5); // 0, 1, 2, 3, 4 のいずれかが返る

その作り方はググれば出てくるのでそちらに任せるとして、今回はそれの型の付け方についての話。

上記の randomInt 関数の型、普通につけるなら下記のようになるでしょう。

function randomInt(n: number): number;

もちろんこれでいいと思いますが、今回はネタ的に「返ってくる可能性がある値」だけを戻り値の型にすることを考えます。

すなわち、先の例だと // 0, 1, 2, 3, 4 のいずれかが返る わけですから、本来は 0 | 1 | 2 | 3 | 4 が正しい(?)わけです。ということで、そうできないか挑戦してみました。


やり方

TypeScriptで最低n個の要素を持った配列の型を宣言する方法 を参考に書いてみました。なお


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


らしいので、randomInt の戻り値としては実用的ではないかと思います。あくまで型パズルネタとしてお楽しみください。

以下で実現できました1

// 参考記事の Append そのままです

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

type NaturalNumberRec<
N extends number, // randomInt に与えられた数
I extends number, // カウンター
C extends unknown[] // カウンター保持用配列
> = {
0: never; // N === I のとき
1: I | NaturalNumberRec<N, PlusOne<N, C>["length"], PlusOne<N, C>>;
}[N extends I ? 0 : 1];

type NaturalNumber<N extends number> = NaturalNumberRec<N, 0, []>;

function randomInt<N extends number>(n: N): NaturalNumber<N> {
throw Error("Not implemented");
}

const a: 0 | 1 = randomInt(2);
const b: 0 | 1 | 2 | 3 | 4 = randomInt(5);

発想としては union 型の結合律(0 | (1 | 2)0 | 1 | 2 であること)を利用して、カウンター変数を使って再帰でループチックなことをしながら union に要素を追加していってるだけです。配列と数値の2つでカウンターを保持しているのは型システム上で数値の計算ができないため配列(タプル)の length を利用する2という、参考サイトの発想そのままです。

PlusOne<N, C> を 2 回書いてる部分はもう少し工夫できそうな気がします。

実行コード風に書くと下記のイメージのことをしています。

function plusOne(i: number): number {

return i + 1;
}

function naturalNumberRec(n: number, i: number): number[] {
return (n === i) ?
[] :
[i].concat(naturalNumberRec(n, plusOne(i)));
}

function naturalNumber(n: number): number[] {
return naturalNumberRec(n, 0);
}

naturalNumber(5); // -> [0, 1, 2, 3, 4]

いかがだったでしょうか?3 こんなことまで出来るなんて、TypeScript は凄いですね。TypeScript の柔軟な型システムを活かして良い TypeScript ライフを!





  1. が、最初の利用箇所で再帰の深すぎワロタエラーが出てしまいます。。。なぜ。。。 



  2. 逆に配列だけあれば I は不要ですが、毎回 length を取るのも面倒なので冗長ですが変数に持たせてみました 



  3. 一度言ってみたかった