1月14日、Node.js開発チームの中心的メンバーであるMarco Ippolito氏が「Everything You Need to Know About Node.js Type Stripping」と題した記事を公開した。この記事では、Node.jsでTypeScriptコードを実行する際に行われる「型除去(Type Stripping)」について詳しく述べられている。
本記事は、以下のエキスパートに監修していただきました:
監修者からの補足:
yosuke_furukawa: Node.js としては毎年ユーザーのアンケートをヒアリングしており、その中でも要望が多かった TypeScript サポートという機能が実装される運びになりました。とはいえ、 TypeScript になっているファイルの型を除去して JavaScript として実行されるので、 TypeScript の強力な型チェックの機構などは含まれていません。 DenoもBunも実行時は型除去が行われて実行されるだけなので、 Node.js もやっとそこに追いついた形になります。しかしながら Deno は少しだけサポートが強く、 deno check
コマンドで型チェック機能もサポートされています。
こういった機能差分はいくつかあります。他にも Node.js では React の JSX はサポートしていません。これらの機能差分はありますが、まずは最低限の機能を追加していき、あとは要望に合わせて改善するという姿勢を見せたことが大きいのではないかと思っています。
uhyo: 記事中で紹介されているように、Node.jsのこの動きはTypeScript側にもオプションの追加といった形で影響を及ぼしています。当初はNode.js側とTS側でうまく連携が取れていないように見えて心配でしたが、落とし所が見つかり前に進んだようで何よりです。
ランタイムのtype stripping機能が出揃った今でも、型チェックのためにTypeScript (tsc) を使うことは引き続き重要です。使用するランタイムに合わせて適切にTypeScriptのオプションを設定することが今後ますます重要になってくるでしょう。
特に、rewriteRelativeImportExtensionsについては、よく考えて使用するように注意喚起されています。本機能による.tsの直接実行と、.ts→.jsという従来のビルドを併用したいときだけ使用するべきです。
以下に、その内容を簡単に紹介する。
TypeStripping概要
2024年8月、Node.jsは長年の課題であった「TypeScriptを設定なしで実行する」問題に対処するため、新しい実験的機能であるType Strippingを導入した。Node.js v23.6.0でデフォルト有効となり、安定版への道を進んでいる。この機能の導入の背景、解決する問題、そしてNode.jsコミュニティに与える影響を掘り下げていく。
なぜTypeScriptサポートが必要か
TypeScriptは非常に高い人気を誇り、最新のNode.js調査でも最も要望の多かった機能の一つである。
Node.jsは以前からローダーを通じてTypeScriptをサポートしてきたものの、設定ファイルやユーザ側ライブラリへの依存が強く、ローダー間の互換性の差異などから開発体験にばらつきがあった。こうした不便さを解消し、コードの書き始めから実行までのサイクルをより高速かつシンプルにするのがこの機能のモチベーションである。
コンパイルとトランスパイル
TypeScriptは静的型付けやその他の機能を追加するJavaScriptのスーパーセットだが、最終的にJavaScriptコードに変換して実行するためのツールチェーンを必要とする。その中核を担うツールがtsc
と呼ばれるコンパイラだ。
tsc
には大きく分けて型チェックとトランスパイルの2つの機能がある。
型チェックはtsc
の実装に強く依存している。TypeScriptの型システムの正式な仕様書というものが存在しない以上、tsc
自身の振る舞いがTypeScriptの型チェックの定義といえる。tsc
はセマンティックバージョニングを採用していないため、マイナーバージョンアップであっても型チェックの変更が加わり、既存のコードが壊れる可能性がある。
一方、トランスパイルはより安定している。型を削除したり、一部の構文を変換したり、JavaScriptエンジンが対応していない新しい構文を古い構文に変換(ダウンレベル化)する、という処理が主に行われる。型チェックほど頻繁に破壊的変更は起こりにくく、特にTypeScriptの型情報削除のみを前提とした“最小限の”トランスパイルであれば、バージョン間のリスクはより低い。
Node.jsがコンパイルや型チェックを行わない理由
TypeScriptサポートについては、これまでにも多くの案が検討されてきたが、いずれもフラグや設定ファイルが必要になるなど、開発者に追加の手間を強いるものだった。
tsc
をNode.jsに直接組み込むという、一見明快な解決策も提案されていたが、大きな問題がある。
第一に、Node.jsは安定性を最優先する。3年近いライフタイムサイクルを持つNode.jsでは、tsc
のマイナーアップデートで型チェックが壊れるリスクを背負うことはできない。
第二に、型チェックはtsconfig.json
と密接に結びついており、その設定がtsc
のバージョンと合っていなければならない。仮にNode.jsがデフォルトのtsconfig.json
を持っていたとしても、ユーザのプロジェクト環境と合わないケースがほとんどだと考えられる。
第三に、tsc
パッケージ自体が約22MBと非常に大きく、Node.jsバイナリに同梱するとサイズ増大を招く。これは特にサーバレス(AWS Lambdaなど)やリソースが限られた環境において深刻な問題である。
Node.jsによるType Stripping
組み込み型チェックには前述のような問題がある一方で、トランスパイル(による型情報の除去など)は非常に軽い処理であるため、Node.jsランタイムが組み込みで提供できる可能性がある。ただし、いくつかの課題をクリアしなければならない。
最も大きな課題は、tsconfig.json
に依存しない形で対応する必要がある点だ。モジュール解決やコンパイラ設定といった「TypeScriptの多様な動作モード」を、Node.jsが裏側で処理しすぎないように調整する必要がある。
こうした問題を解消するため、Node.jsはまず--experimental-strip-types
という実行時フラグを導入した。このフラグをオンにすると、TypeScriptファイルを「その場で型だけを削除して実行」でき、これを **Type Stripping(型情報の除去)**と呼ぶ。実行時に型チェックや複雑なトランスパイルを行わないことで、TypeScriptのマイナーアップデートに追随するリスクを抑えつつ、開発者は追加設定なしでコードを実行できるようになった。
また、一般的なトランスパイルではソースマップを生成・読み込みし、コード位置を復元している。ソースマップがないと、エラー発生時のスタックトレースが実際に書いたコードとずれてしまい、デバッグが難しくなる。しかしソースマップ生成にはオーバーヘッドがある。
Node.jsでは、削除した型の部分を単純に空白で置き換えることで「コードの行番号をずらさない」工夫を行っており、ソースマップを不要にしている。つまり、実際に実行されるコードは、開発者が記述したもの(ただし型定義は削除済み)と行・列位置が同一になり、デバッグが容易になる。
この手法は、ts-blank-space
(Ashley Claymore氏による)を参考にしたものである。
トランスパイルにはRustで書かれた高速・軽量なコンパイラ「swc」のカスタム版を利用している。swcはWebpackやRollup、Turbopack、rspack、Denoなど多数のプロジェクトで実績があり、Node.jsに組み込むに当たっては、swcの開発者であるDongYoon Kang氏が直接支援した。
Node.jsによるTypeScriptサポートの実際
とは言え、あらゆるTypeScriptコードをそのままNode.js上で実行できるようになったわけではない。Node.jsランタイムは、サポートできる構文を制約するなどのトレードオフにより、TypeScript特有の複雑な機能を取り込むことなく、パフォーマンスを維持しつつ柔軟性を確保している。
例えばTypeScriptの一部機能は、単純に「型を削除する」だけでは済まない。
たとえばenum
やネームスペースの実装、パラメータプロパティなどは、実行時の動作に影響するコードを生成するため、単なる型削除ではカバーできない。
こうした機能を利用する場合は、実行時にソースマップが必要となるトランスフォーメーションを避けられない。そのためNode.jsでは--experimental-transform-types
というフラグを設け、ソースマップを有効にしたうえでこれらを含むトランスパイルを行うオプションを提供している。--experimental-strip-types
はデフォルトの動作で、まずはコード生成を伴わない新しいTypeScript構文(enumなどを使わないモダン構文)を試してほしいという意図がある。
下記はTypeScriptのenum
がJavaScriptにコンパイルされる例である。
enum Direction {
Up = 1,
Down,
Left,
Right
}
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
Direction[Direction["Left"] = 3] = "Left";
Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
また、TypeScriptにはECMAScript仕様上ではまだ標準化の途上にある機能も一部利用することができる。こうした機能は、既存のJavaScriptコードに変換して利用する「ダウンレベル化」して使われることが一般的だ。たとえばDecoratorsなどはTC39のStage 3提案だが、TypeScriptでは既にサポートされている一方、V8(Node.jsのJSエンジン)ではまだ正式対応されていない。
Node.jsは現在ダウンレベル化をサポートしていない。その理由は、実装と仕様とのズレによる将来的なリスクを回避するためだ。いずれJavaScriptエンジンがネイティブ対応するまで待つほうが安全である。
余計なマジックを避け、振る舞いを予想可能にする
Node.jsは開発者のコードを自動的に変換してくれるものではない。あくまでも「書いた通りのコードをそのまま実行する」ことを重視しており、余計なマジックを避けている。
Type Strippingは、JavaScriptとして実行可能な形を保ちながら型だけを消すため、 通常のJavaScriptと極めて似通ったコードが保たれる 利点がある。たとえば、インポート時に.ts
拡張子を省略することは許されていない。これを許容するとどのファイルが対象か曖昧になり、潜在的な問題を引き起こすためだ。そのため、インポートするファイルには明示的に.ts
を付ける必要がある。
これは一見面倒な制約であるが、実際には.js
ファイルと.ts
ファイルが共存できるという大きなメリットがある。
また、ランタイムで型情報は消去されてしまうため、型定義をインポートする場合にはtype
キーワードを使わなければならない
// これは正しい
import { Foo, type FooType } from './foo.ts';
import type { MyType } from './types.ts';
// これはランタイム時にエラーとなる
import { Foo, FooType } from './foo.ts';
import { MyType } from './types.ts';
type
キーワードを付けずにインポートすると、実在しない値をインポートすることになり、実行時エラーが発生する。
tsconfig.json
でverbatimModuleSyntax
を有効にしていると、不要なimportによるエラーをあらかじめ検出してくれる。
ESLintの@typescript-eslint/no-import-type-side-effects
などのルールもあわせて導入すれば、型定義のみが目的のファイルを実行時にインポートしてしまうミスを防げる。
今後の方向性
以上で述べたような、Node.js上でTypeScriptを実行できるようにするための一連の取り組みは、TypeScriptチームからも直接サポートを受けている。たとえばTypeScript 5.7ではrewriteRelativeImportExtensions
というフラグが導入され、.ts
拡張子を.js
に書き換えて実行ファイルを生成しやすくなっている。これはNode.jsのType Strippingを補完する位置づけだ。
今後のTypeScriptバージョン(おそらく5.8)では、enum
やネームスペース、パラメータプロパティなどの「実行時にコード生成される機能」をエラーにする--erasableSyntaxOnly
フラグが検討されている。これが実装されれば、実行時ではなくエディタやビルド時点で違反を指摘できるようになり、より開発体験が向上するだろう。
また、Node.jsにおけるTypeScriptのコンパイルキャッシュも検討されている。これはコンパイル後の結果を再利用してモジュールのロードを高速化するもので、リピート作業の多いシナリオでパフォーマンスが大きく向上する可能性がある。
これらの機能はまだ実験的要素が多いが、ロードマップを確認すると、今後の進化の方向性がうかがえる。
まとめ
大規模で多様なユーザを抱えるNode.jsでは、エコシステムを壊さないよう慎重に新機能を実装する必要がある。見た目以上に入り組んだプロセスを経て、このType Stripping機能は着実に進化を続けている。
詳細は[Node.js Type Stripping Explained」を参照していただきたい。
監修しました。 uhyo さんと連名という初の監修スタイル。 / “【海外記事紹介】Node.jsでTypeScriptを実行したい?ならばType Strippingについて知っておこう” htn.to/2hKHNvi2Gp
この前のNode.jsのtype strippingの記事が読みやすい日本語記事になった。
監修という形で好き勝手コメントさせてもらったのであわせて確認しよう(?)
techfeed.io/entries/6786e2…