TypeScript で型レベル FizzBuzz


動機と目的

TypeScript 初心者なので、FizzBuzz を書こうと思いました。
TypeScript にはリテラル型とタプル型があるので、[1, 2, "Fizz", 4, "Buzz", ...] という FizzBuzz 型を生成することができるはずです。
型のインスタンス化の制限のため、あまり長くは生成できそうにありませんが、2 回目の "FizzBuzz" くらいまではたどり着きたいです。
外部ライブラリは使用しません。

結果

今の私の TypeScript 力で、FizzBuzz 感を出しつつ 2 回目の "FizzBuzz" まで生成しようとすると、こんな感じになりました。

fizzbuzz.ts
type Buzz<N, ZS1 extends readonly any[] = [0]> =
  {
  0:
    []
  1:
    ((z1: 0, ...zs1: ZS1) => any) extends ((...zs2: infer ZS2) => any) ?
    ((z2: 0, ...zs2: ZS2) => any) extends ((...zs3: infer ZS3) => any) ?
    ((z3: 0, ...zs3: ZS3) => any) extends ((...zs4: infer ZS4) => any) ?
    ((z4: 0, z5: 0, ...zs4: ZS4) => any) extends ((...zs6: infer ZS6) => any) ?
    Buzz<N, ZS6> extends infer BS6 ?
    BS6 extends readonly any[] ?
    ((b1: ZS1['length'], b2: ZS2['length'], b3: ZS3['length'], b4: ZS4['length'], b5: 'Buzz', ...bs6: BS6) => any) extends ((...bs1: infer BS1) => any) ?
    BS1 :
    never : never : never : never : never : never : never;
  }[ZS1['length'] extends N ? 0 : 1];

type Fizz<BS1 extends readonly any[]> =
  {
  0:
    [];
  1:
    ((...bs1: BS1) => any) extends ((b1: infer B1, b2: infer B2, b3: infer B3, ...bs4: infer BS4) => any) ?
    Fizz<BS4> extends infer FS4 ?
    FS4 extends readonly any[] ?
    ((f1: B1, f2: B2, f3: B3 extends 'Buzz' ? 'FizzBuzz' : 'Fizz', ...fs4: FS4) => any) extends ((...fs1: infer FS1) => any) ?
    FS1 :
    never : never : never : never;
  }[BS1['length'] extends 0 ? 0 : 1];

// 'N' shall be of the form '15K + 1'.
type FizzBuzz<N> = Fizz<Buzz<N>>;

const fizzBuzz: FizzBuzz<31> = 'FizzBuzz';

[email protected]実行しました。

% tsc fizzbuzz.ts
fizzbuzz.ts:34:7 - error TS2322: Type '"FizzBuzz"' is not assignable to type '[1
, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzB
uzz", 16, 17, "Fizz", 19, "Buzz", "Fizz", 22, 23, "Fizz", "Buzz", 26, "Fizz", 28
, 29, "FizzBuzz"]'.

33 const fizzBuzz: FizzBuzz<31> = 'FizzBuzz';
         ~~~~~~~~


Found 1 error.

できていそうです。

解説

TypeScript のままだと読みにくいので、TypeScript に書き直します。

function buzz(n: number, zs1: readonly 0[] = [0]): readonly ('Buzz' | number)[] {
  if (zs1.length === n) {
    return [];
  } else {
    const zs2 = [0, ...zs1] as const;
    const zs3 = [0, ...zs2] as const;
    const zs4 = [0, ...zs3] as const;
    const zs6 = [0, 0, ...zs4] as const;
    const bs6 = buzz(n, zs6);
    const bs1 = [zs1.length, zs2.length, zs3.length, zs4.length, 'Buzz', ...bs6] as const;
    return bs1;
  }
}

function fizz(bs1: readonly ('Buzz' | number)[]): readonly ('FizzBuzz' | 'Fizz' | 'Buzz' | number)[] {
  if (bs1.length === 0) {
    return [];
  } else {
    const [b1, b2, b3, ...bs4] = bs1;
    const fs4 = fizz(bs4);
    const fs1 = [b1, b2, b3 === 'Buzz' ? 'FizzBuzz' : 'Fizz', ...fs4] as const;
    return fs1;
  }
}

// 'n' shall be of the form '15k + 1'.
function fizzBuzz(n: number): readonly ('FizzBuzz' | 'Fizz' | 'Buzz' | number)[] {
  return fizz(buzz(n));
}

console.log(fizzBuzz(31));

自然数列を生成する方法が肝のように思います。
自然数から直接次の自然数を得ることはできないようなので、今回は適当な配列を伸ばしていき、その length を取ることで自然数列を作っています。

また 5 ずつまとめて自然数を生成することで、型のインスタンス化の制限を避けようとしています。ついでに "Buzz" も埋め込んでいます。
もっと言えば 15 ずつ処理することもできるはずですが、個人的に FizzBuzz してる感が薄れるので、やっていません。

まとめ

TypeScript で FizzBuzz をやってみました。