スプーキーズのちょっとTech。

SPOOKIES社内のより技工的な、専門的なブログページです。

「Type」ScriptでHelloWorldをしてみた Part2

皆さんはTypeScript、使っていますか?TypeScriptっていいですよね。 JavaScriptに型をつけ、ソースコードの型を解析し、実行前に型的な部分の予期しないエラーを防いでくれます。つまり、JavaScriptを静的型付け言語にしたのがTypeScriptというわけです。

ですが皆さん、ちゃんとTypeScriptしてますか??? 日本人が使う/触ったことのある静的型付け言語の代表格といえばC言語(C++)、Java(C#)あたりじゃないでしょうか。難しかった経験、ありますよね?主に「型」への理解が難しくて、PythonやPHPといった型のない世界に逃げ込んだ方が多いんじゃないでしょうか。(どちらも動的型付け言語ではありますが、デフォルトで実行前に型チェックが入っているわけではないので「型がない」と表記しています。) JavaScriptも同じで、もともと型というものは存在していません。ですがTypeScriptにおいては明確に「型」が存在しています。つまり、土台はJavaScriptですが、言語設計としてはちゃんと「型」を使った開発をすることが求められるわけです。

なので皆さん、「「ちゃんとTypeScriptしてますか???」」

まあ変な前置きはここまでにしておいて、 この記事はPart1の「「Type」ScriptでHelloWorldをしてみた」の続編です!

labs.spookies.co.jp

まだPart1見てないよ~って方はPart1を見てからじゃないと内容を理解できないのでそちらをご覧ください~

次にするのは、typeで四則演算を行うtypeを作りたいと思います。 typeで四則演算ってできるの!?って思うかもしれないですが、厳密には自然数だけで割り算以外であればとっても簡単に作れちゃうんです。(割り算は少数になる可能性があり考慮することが多い)

2. 四則演算と数値を作る

2-1. Tupleを錬成する!

type GetRange<N, Acc> =
  Acc extends number[]
    ? Acc[Length] extends N
      ? Acc
      : GetRange<N, [...Acc, Acc[Length]]>
    : never;

Tuple型は再帰を使うと簡単に要素数を増やすことができます! 再帰系型の中でも、Rangeは一番有名じゃないでしょうか。

1. <3, []> => [0]
2. <3, [0]> => [0, 1]
3. <3, [0,1]> => [0, 1, 2]
4. <3, [0,1,2]> => return

指定された数値リテラルのLengthとなるまで「その時配列に入っている要素数」を配列内に入れるという動作を繰り返すと、Rangeした結果のTupleが返却されます。

言ってしまえばこのRangeを取得したいがためにLengthを定義したまでのことはあります。これがないと難しいのじゃ

2-2. 加算と減算を作る!

type Add<Left, Right> = [
  ...GetRange<Left, []>,
  ...GetRange<Right, []>
][Length];

type Sub<Left, Right> =
  GetRange<Left, []> extends [
    ...GetRange<Right, []>,
    ...infer R
  ]
    ? R[Length]
    : never;

Rangeと同じ要領で加算と減算を作成します。

加算はLength + Length、つまり左の数分のTupleと右の数分のTupleをスプレッド構文で展開すると、最終的なlengthは左のTupleのlengthと右のTupleのlengthを足し合わせた状態になります。

3 + 4 => [1,2,3] + [1,2,3,4]
[...[1,2,3], ...[1,2,3,4]] => [1,2,3,1,2,3,4] => 7

減算は、逆に、左のTupleから右のTuple分を消すと最終的な要素数が左の要素数から右の要素数を引いた数になります。 感覚的には、掛け算の約分に近い感じで、同じ要素を取り除くようなイメージです。

5 - 3 => [1,2,3,4,5] - [1,2,3]
[1,2,3,4,5] extends [[1,2,3], ...infer T] => [4,5] => 2

2-3. 0と1を錬成する!

今回の縛りでは、数値リテラルは直接定義できません。じゃあどうするか? Tupleからとってくればいいんです。

type Zero = [][Length]; // 0
type One = [true][Length]; // 1

簡単ですね! ただ、これを何回もやってると長いので、先ほどの計算が生きてくるわけです。 その中でも0と1は数値リテラルの種的な扱いをするのでちょっと特殊なだけです。

2-4. 乗算を作る!

ひたすらに加算しまくるのもめんどくさいので乗算も作っちゃいましょう!

type Mul<Left, Right, Total> =
  Right extends Zero
    ? Total
    : Mul<Left, Sub<Right, One>, Add<Total, Left>>;

これは右側の回数分再帰をして、0になるまでTotalに左の数を加算し続けているだけです。

2 * 3 => 2 + 2 + 2 => 6
8 * 8 => 8 + 8 + 8 + 8 + 8 + 8 + 8 + 8 => 64

あまりやりすぎると、再帰エラーになってしまうので気を付けましょうね...!

2-5. 2と4を作る!

最後に作った四則演算を使って2と3を作ってみましょ。

type Two = Add<One, One>; // 1 + 1 = 2
type Four = Mul<Two, Two, Zero>; // 2 * 2 = 4

わりと直感的に数値を操作できるようになりましたね!いい話 まあ4に関しては別に使う必要ないんですけどね...

3. Hello Worldを錬成する

さあいよいよHelloWorldを錬成する時が来ました!

3-1. 文字リテラルから特定インデックスの文字を取得する関数を作る!

今までの知識を使って文字リテラルのchatAtを作成しましょう!

type CharAt<Str, Index> =
  Index extends Zero
    ? Str extends `${infer FirstChar}${string}`
      ? FirstChar
      : never
    : Str extends `${string}${infer Remain}`
      ? CharAt<Remain, Sub<Index, One>>
      : never;

割と単純ですが、Indexと文字リテラルの頭を引いて行って、Indexが0になったときの頭文字を取得するという感じです。

1. <"abcde", 3> => <"bcde", 2>
2. <"bcde", 2> => <"cde", 1>
3. <"cde", 1> => <"de", 0>
4. <"de", 0> => "d"

今まで出てきた知識を使っているだけなので、勝手がわかれば簡単ですね~

3-2. 中間ポイントを計算しておく

文字テーブルを使うにあたって中間地点からアクセスできるようにちょっとだけ計算しておきます。

type Calc32 = Mul<Mul<Two, Four, Zero>, Four, Zero>;
type Calc20 = Add<Mul<Four, Four, Zero>, Four>;

3-3. 文字を1つづつ取得する!

文字テーブルからcharAtでHelloWorldを取得していきます! ここはもう今まで出てきたのを組み合わせてくだけなので余裕です。

type CharTable = "abcdefghijklmnopqrstuvwxyz!? ";
type h = CharAt<CharTable, Sub<Mul<Four, Two, Zero>, One>>;
type e = CharAt<CharTable, Four>;
type l = CharAt<CharTable, Sub<Mul<Four, Four, Zero>, Add<One, Four>>>;
type o = CharAt<CharTable, Sub<Mul<Four, Four, Zero>, Two>>;
type sp = CharAt<CharTable, Sub<Calc32, Four>>;
type w = CharAt<CharTable, Add<Calc20, Two>>;
type r = CharAt<CharTable, Add<Sub<Calc20, Four>, One>>;
type d = CharAt<CharTable, Add<Two, One>>;
type ex = CharAt<CharTable, Sub<Calc32, Add<Four, Two>>>;

3-4. 組み合わせる!

最後に、先ほど取得した文字を結合していくだけ!

type Result = TupleJoin<[
  Capitalize<TupleJoin<[h, e, l, l, o]>>,
  sp,
  Capitalize<TupleJoin<[w, o, r, l, d]>>,
  ex
]>;

これで、「"Hello World!"」の文字リテラルが推論できました!!!!!!わーい

終わりに

今回はTypeScriptのtype縛りでHello Worldをしてみました! TypeScriptでもここまでのtypeを構築することはそうそうないので、かなりTypeScriptでの型推論という部分に関して深掘りして行けたかなぁ~と思っています。 これをすぐに理解できる方はきっとTypeScriptのプロなんでしょう。 逆にこの記事を理解できれば少なくとも上級者レベルかも???

次は何の言語で遊ぼうかなぁ~