Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Pipetype: TypeScript Unions Bitwise Operators, and My Favorite Failed Experiment

2025年09月26日に「Dev.to」が公開したITニュース「Pipetype: TypeScript Unions Bitwise Operators, and My Favorite Failed Experiment」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

TypeScriptのユニオン型とJavaScriptのビット演算子「`|`」の共通点に着目し、ランタイムでTypeScriptのような型検証を実現する実験「Pipetype」を行った。BigIntとProxyを使い構文を簡略化したが、TypeScriptの型推論が機能せず、実用化には至らなかった。

ITニュース解説

TypeScriptのUnion型とJavaScriptのビット演算子における「|」記号は、同じ見た目を持ちながらも異なる役割を果たす。TypeScriptでは「string | number」のように複数の型の中からいずれか一つであることを示すUnion型として機能する。これは「どちらか一方」という意味合いである。一方、JavaScriptのビット演算においては、「1 | 2」のように二つの数値のビットを比較し、いずれかのビットが1であれば結果の対応するビットも1にする、ビット単位の論理和(OR)を表す。これは複数の「スイッチ」を同時にオンにするような「両方同時に」というイメージに近い。

この記事では、この二つの異なる「|」の機能が持つ共通の「複数の選択肢を扱う」という側面に着目し、TypeScriptのUnion型が持つ柔軟性をJavaScriptのランタイム(プログラム実行時)で再現できないかという実験が行われた。具体的には、ビット演算の特性を利用して、複数の型情報を一つの数値(ビットマスク)で表現し、それをランタイムで利用するという試みである。

まず、型をビットで表現するというアイデアがある。各型にユニークなビット(スイッチ)を割り当て、複数の型を組み合わせる場合は、対応するビットをすべてオンにした一つの数値で表現する。例えば、「string」型を1番目のビット、「number」型を2番目のビットとすると、string型は「0001」、number型は「0010」と表現できる。そして、string型とnumber型を組み合わせたUnion型は、それぞれのビットを立てた「0011」という数値で表現される。

このビット演算を使った方法には、JavaScriptの通常の数値型(Number型)では扱えるビット数に制限があるという問題があった。Number型では安全に扱える整数が53ビットまでであるため、表現できる型(スイッチの数)が限られてしまう。しかし、多くの異なる型を定義したり、それらを組み合わせたりする場合、53ビットではすぐに足りなくなる可能性がある。この問題を解決するために登場したのがBigIntである。BigIntはJavaScriptで導入された新しいデータ型で、桁数に制限なく大きな整数を扱える。これにより、事実上無限のビットを扱うことが可能になり、非常に多くの型をビットで表現できるようになる。BigIntが、この実験の基盤を支えることになった。

具体的に各型にユニークなビットを割り当てる方法として、ビットシフト演算が利用される。初期値として「1n」(BigIntの1)を使用し、これを左にシフトしていく。「1n << 0n」は「0001」となり、「1n << 1n」は「0010」、「1n << 2n」は「0100」となる。このように左にシフトするたびに、結果は2のべき乗となり、常に一つのビットだけが1になるユニークなビットパターンが生成される。この特性を利用して、各型に重複しないビットフラグを割り当てることが可能になる。例えば、「string」型には「1n (0001)」、「number」型には「2n (0010)」、「Array」型には「4n (0100)」といった具合である。

これらのビットフラグと、そのフラグに対応する型をチェックする関数(バリデーター)はMapオブジェクトに格納される。Mapは、キーと値のペアを保持するデータ構造であり、ここではビットフラグをキーに、型チェック関数を値に持つことで、「このビットフラグが立っている場合は、この型チェック関数を実行する」という対応付けを実現する。手動でビットシフトを計算してフラグを生成するのは手間がかかるため、getNextFlag()というヘルパー関数が用意された。この関数は、呼び出されるたびに自動的に次のユニークなビットフラグ(2のべき乗)を生成して返すため、開発者はビットシフトの計算を意識せずに型を登録できるようになった。

型を登録した後は、それらを組み合わせたり、特定の型が含まれているかをチェックしたりする操作が行われる。「string | number」のように複数の型を組み合わせる場合は、それぞれの型のビットフラグをビット単位の論理和|で結合する。例えば、string型が「0001」、number型が「0010」であれば、それらを結合した結果は「0011」となり、この一つの数値が「stringまたはnumber」という型情報を表現することになる。そして、特定のデータがこの結合された型に含まれるかをチェックするには、ビット単位の論理積&を利用する。例えば、「0011」という結合された型情報に対し、「number」型を表す「0010」をビット単位の論理積で計算すると「0010」という非ゼロの値が返される。これは「number」型が含まれていることを意味する。もし「Array」型を表す「0100」と論理積をとると「0000」が返され、「Array」型は含まれていないと判断できる。

このようにビット演算とBigInt、Mapを使ってランタイムでの型チェックの仕組みは構築できたが、開発者が毎回ビットフラグを意識して記述するのは煩雑である。そこで、Proxyオブジェクトが導入された。ProxyはJavaScriptの強力な機能の一つで、オブジェクトに対する操作(プロパティへのアクセスなど)を途中で横取りし、カスタムの動作を定義できる。この実験では、Type.stringのようにプロパティにアクセスした際にProxyがそれを検知し、自動的に新しいビットフラグを生成してMapに登録し、そのビットフラグを返すように設定された。これにより、開発者はType.string | Type.numberという、TypeScriptの構文に非常に近い形でランタイムの型を定義できるようになり、内部の複雑なビット演算を意識せずに済む「シンタックスシュガー」が実現された。

この時点では、ランタイムでのUnion型表現、BigIntによる無限のバリデーター対応、TypeScriptのような記述性という、まるで魔法のような機能が実現できたように思われた。しかし、この実験は最終的に「失敗」と判断された。その最大の理由は、TypeScriptの型システムが、BigIntで表現されたビットマスクの情報を理解できなかったことにある。TypeScriptのコンパイラから見ると、これらのビットマスクは単なるBigInt型の数値であり、その数値がどのような型情報を含んでいるのか、あるいは複数の型を組み合わせたものなのかを推論することができなかったのだ。

その結果、TypeScriptの主要な利点である型推論や、条件に応じて変数の型をより狭い範囲に絞り込む「型ナローイング」といった機能が全く働かなかった。例えば、if文で型チェックを行ったとしても、TypeScriptコンパイラは変数の型を絞り込むことができず、開発者は型情報を手動でアサーション(型を強制的に指定する)するか、複雑な型定義を記述する必要が生じた。これは、当初期待していた「エルゴノミクス(使いやすさ)」を著しく損なうものであり、便利な記述に見えたシンタックスシュガーも、結局はTypeScriptの恩恵を受けられない「脆いランタイム計算」に過ぎないという結論に至った。

この実験は目標達成には至らなかったが、多くの貴重な教訓を残した。一つは、|記号が持つ言語学的、そして技術的な二面性の深さである。また、BigIntがもたらすJavaScriptの可能性、特にビット演算の領域での表現力の拡大も示された。さらに、Proxyがいかに柔軟な「シンタックスシュガー」を提供し、既存の言語構造を模倣できるかということも明らかになった。時には、目標を達成しない実験が、技術の真の限界や特性を教えてくれる最も価値のある学びとなることを、この「Pipetype」という失敗した実験は示している。

関連コンテンツ

関連IT用語

関連ITニュース