JavaScriptとTypeScriptで全く異なる処理を実行するPolyglotコードの仕組みを徹底解説

元記事を読む
キュレーターコメント

「JavaScriptとTypeScriptで全く異なる動作をするコードを書けるか?」という問いへの完璧な回答。実用ゼロでも、字句解析・型システム・ホイスティングへの深い理解がないと到達できない境地であり、言語仕様を極めたいエンジニアには必読の一本。

概要

「同じコードファイルなのに、JavaScriptで実行すると Hello, JavaScript! と表示され、TypeScriptで実行すると Hello, TypeScript! と表示される」——そんな魔法のようなコードが実際に書けると聞いたら、あなたはどう思うだろうか?

これは Polyglot(ポリグロット) と呼ばれるテクニックの一種だ。複数のプログラミング言語で解釈できるプログラムを指し、多言語話者(polyglot)になぞらえてこの名が付いている。実用性は皆無に近いが、言語の構文・字句解析の深部まで理解していないと到底作れない、ある種の「パズル」として知る人ぞ知る遊びだ。今回 Zenn に投稿された記事では、JavaScript と TypeScript のあいだで全く異なる処理を実行し、かつ 型エラーが一切発生しない Polyglot コードの完全解答が公開された。

実際のPolyglotコード

まず答えを見てほしい。

new (f())<Number>0 > /[/*]/
function f() { console.log("Hello, JavaScript!"); return Number; }
// */].length, `/*`
function f() {
  console.log("Hello, TypeScript!");
  return Number as unknown as new <T>() => number;
}
//*/
  • JavaScript として実行Hello, JavaScript! が表示される
  • TypeScript として実行Hello, TypeScript! が表示される
  • 型エラーなし

これがどうして動くのか、順を追って解説する。

仕組みの核心:JavaScriptの「状態付き字句解析」

JavaScript のパーサーは文脈によって同じ文字を全く異なるトークンとして解釈する。特に重要な2点がある。

  • / の解釈:式が期待される位置では 正規表現リテラル の開始、演算子が期待される位置では 除算演算子 になる
  • } の解釈:通常は閉じ波括弧だが、テンプレートリテラルの式補間中では テンプレートの後続 として扱われる

つまり、パーサーの「期待する状態」を操作できれば、同じ文字列を全く異なるコードとして解釈させることができる。これがこのPolyglotの根幹だ。

TypeScript 4.7 の「インスタンス化式」を悪用する

TypeScript は厳密には JavaScript の完全なスーパーセットではなく、いくつかの 構文的曖昧性 がある。その一つが TypeScript 4.7 で導入されたインスタンス化式 だ。

f<Number>  // ジェネリック型を呼び出さずに型の具体化だけを行う

この構文のポイントは、式全体が > で終わること。これにより後続に任意の演算子を置ける。

  • TypeScript では:f<Number> → インスタンス化式として解釈(0 > /regex/ は比較演算)
  • JavaScript では:f<Number>0< が比較演算子 → f より小さいか比較した後、> が別の比較演算子になり、/[/*]/ が正規表現リテラルとして解釈される

この解釈の分岐が、字句解析器の「状態」を変える起爆剤になる。

コメントの切り替えで「#else」相当を実現

字句解析の分岐を利用して /[/*]/ 内の /* をコメント開始に使う巧妙なトリックがある。

  • JavaScript(除算として解釈)/* がコメント開始になり、その後のコードがコメントアウトされる
  • TypeScript(正規表現として解釈)/* は文字クラス [/*] の一部なので正規表現内に閉じ込められ、コメントは開始されない

さらに // */].length, \/*“ という一行が #else スイッチとして機能する。

  • コメント外の状態で読まれると // で行コメント開始 → 次の行の /* で再びブロックコメント開始
  • コメント内の状態で読まれると */ でコメント終端 → バッククォートでテンプレートリテラル開始 → 次行の /* をスキップして再度バッククォートで終端

これにより、JavaScript と TypeScript でそれぞれ異なる function f() の定義が有効になる。

型エラーを消す最後の工夫

型検査を通すために function 宣言の ホイスティング を活用している。new (f()) で関数呼び出し結果をコンストラクタとして使い、TypeScript 側の f()Number as unknown as new <T>() => number という型キャストを行う。function 宣言はファイル先頭に巻き上げられるため、コード上の順序を逆にしてもTypeScriptの型チェックが通る。また ].length を付け加えることで、配列リテラルを閉じつつ結果を number 型として整合させている。

まとめ・所感

実用性はゼロに等しいが、このPolyglotコードを読み解くには以下の深い知識が必要だ。

  • JavaScript の stateful lexer の挙動(正規表現 vs 除算)
  • TypeScript 4.7+ のインスタンス化式の仕様
  • ブロックコメント・テンプレートリテラルの字句解析規則
  • function 宣言のホイスティング

これを「遊び」と言い切りながら完璧に仕上げてしまう技術力には純粋に唸らされた。TypeScript は「JavaScript の完全なスーパーセット」ではないというのは頭では知っていても、ここまで突き詰めると両者の解釈が本当に別物になる瞬間がある。言語の構文仕様をここまで深く探求したい人は、ぜひ元記事の TypeScript Playground リンクで実際に動作を確認してみてほしい。