この記事は NTTドコモソリューションズ AdventCalendar 2025 14 日目の記事です。
はじめに
NTTドコモソリューションズの熊渕です。
普段はウェブクライアント関連の問い合わせサポートや技術調査をしています。
この記事では Node.js v25.2.0 で安定版となった TypeScript をネイティブに実行するための機能「Type Stripping」について簡単にご紹介したいと思います。
Type Stripping とは
これまで Node.js で TypeScript を実行するには、ts-node や tsx といったツール、あるいは TypeScript コンパイラである tsc などを使って JavaScript へトランスパイルする必要がありました。しかし、2025 年 11 月 11 日にリリースされた Node.js v25.2.0 では外部ツールなしで TypeScript を直接実行できる機能「Type Stripping」が安定版 (Stable) となりました。
Type Stripping とはその名の通り「TypeScript コードから型注釈(Type Annotations)だけを削除し、JavaScript エンジン(V8)が理解できる形にする」処理のことです。この機能のポイントとなっているのは、Node.js ランタイムは型チェックを行わないという点です。型チェックは IDE や CI/CD パイプラインで行うものとし、ランタイムはあくまで型注釈が削除された TypeScript コードを JavaScript として実行する方針をとっており、これにより型チェックやトランスパイル処理を省略した高速な起動とシンプルなランタイム実装が実現されています。
百聞は一見にしかず。以下の TypeScript コードを直接 Node.js で実行してみます。
const message: string = "Hello, Type Stripping!";
console.log(message);
$ node -v
v25.2.0
$ node test.ts
Hello, Type Stripping!
そのまま実行できました。これは非常にシンプルな例ですが、上記のように v25.2.0 以降の Node.js では簡単な TypeScript コードであれば外部ツールやトランスパイル無しで直接実行が可能となっています。
Type Stripping を支える技術
これで記事を終わろうかと思いましたが、それだと寂しすぎるので技術的な側面にも触れておきます。
Amaro
高速な Type Stripping を実現するために、Rust 製のツールである SWC がベースの技術として採用されています。Node.js 内部で TypeScript パーサーとして利用されていますが、Rust のツールをそのまま Node.js に組み込もうとしたところ Node.js がサポートするプラットフォームのすべてをカバーすることができなかったため、実際には Wasm(WebAssembly)にポートされた @swc/wasm-typescript が導入され、その後型除去の機能をユーザーが個別に利用できるよう「Amaro」というパッケージとして切り出されました。
これにより Node.js がサポートする幅広いプラットフォーム上で動作が可能な形で Type Stripping の実装が進められました。
「Node.js に tsc を内蔵してしまえばいいのでは?」と考える方もいるかもしれません。しかし、これには技術的に大きな壁があったそうです。
安定性の保証: Node.js は長期サポート(LTS)を提供する安定したプラットフォームである一方で、tsc はセマンティックバージョニングに従っておらず、マイナーアップデートで破壊的変更が入ることもあったため Node.js のランタイムに組み込むことはリスクが高かった。
構成の密結合: 型チェックは tsconfig.json に強く依存するが、Node.js がデフォルトの設定を持つことはユーザーのプロジェクト設定と競合する可能性が高く、実用的ではなかった。
バイナリサイズとパフォーマンス: tsc を含めると、Node.js のバイナリサイズが約 22MB 程度増加し、起動時間も遅くなってしまった。
そこで採用されたのが、前述した通り「Type Stripping(型情報の削除)」というアプローチだったとのことです。
「空白置換」によるソースマップの不要化
通常 TypeScript を JavaScript にトランスパイルするとコードの行数や文字数が変わってしまいます。そのため、通常であればエラー発生時に元のコードの位置を知るための「ソースマップ」が必要になりますが、ソースマップの生成と読み込みはパフォーマンス上の大きなオーバーヘッドになります。
そこで型情報の削除において、実際には「型定義を空白(スペース)に置換する」という方法が採用されています。
例えば、以下のようなコードがあったとします。
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
これを Type Stripping すると以下のようになります。
function printLabel(labeledObj ) {
console.log(labeledObj.label);
}
上記の通り、型注釈の部分がそのまま空白に置き換わっています。これにより行番号と列番号が元の TypeScript ファイルと完全に一致し、重いソースマップを生成・解析することなく正しいスタックトレースを表示できるようになっています。
実用における制約
Type Stripping はいわば「型を消すだけ」という単純な仕組みのため、実用においては現状では色々と制約や注意点があります。
サポートされていない TypeScript の構文
まず TypeScript 固有の構文等はサポートされていません。例えば以下に記載するような TypeScript 固有の機能については単純な型情報の削除だけでは JavaScript として動作しないため、もし TypeScript のコード内で利用されている場合はデフォルトでは動作しません。
- Enum(列挙型)
- Namespace(名前空間)
- コンストラクタのパラメータプロパティ
- インポートエイリアス
上記機能が含まれている場合は JavaScript でサポートされている構文を使った形に修正するか、下記のように --experimental-transform-types フラグを使用する必要があります。
$ node --experimental-transform-types app.ts
このフラグを使用した場合はコード変換が行われるため、ソースマップが有効になり Type Stripping と比べて実行時のオーバーヘッドが大きくなります。
TypeScript v5.8 Beta 以降では、Node.js で型情報の削除により実行可能な構文だけで書かれているかを確認できるオプションとして erasableSyntaxOnly が導入されました。本オプションを有効にしておくと、上記のような Type Stripping で実行可能ではない構文を使うとトランスパイル時にエラーとなります。
インポート時の注意点
インポートの記述方法にも注意が必要です。
拡張子の明記
下記のように、インポート文には .ts 拡張子を明記する必要があります。
import { foo } from './foo.ts';
従来は TypeScript で実装しているプロジェクトであっても import 宣言の相対パスに拡張子を記載する場合は .js を使う必要がありました。これは TypeScript がランタイムで実行される際には JavaScript にトランスパイルされていることを念頭においた仕様ですが、これに対し TypeScript 5.7 では Type Stripping を利用するユーザーに向けて rewriteRelativeImportExtensions オプションが導入され、トランスパイル時に .ts を .js に書き換えることが可能になりました。
型のみのインポート
型定義だけをインポートする場合は、必ず type キーワードを使用する必要があります。これはランタイムで実行した際に、インポート先のファイルの中身が Type Stripping により削除されてしまった場合にエラーとなってしまうのを防ぐためです。type キーワードによりインポートされている箇所は、型のインポート自体が Type Stripping により削除され、実行時のインポートエラーを防止します。
以下のコードは OK ですが、
import type { Type1, Type2 } from './module.ts';
import { fn, type FnParams } from './fn.ts';
以下のコードは NG です。
import { Type1, Type2 } from './module.ts';
import { fn, FnParams } from './fn.ts';
TypeScript の verbatimModuleSyntax オプションにより、型のみのインポートに type の使用を強制することができます。
node_modules 内の TypeScript は実行不可
パフォーマンスとライブラリの互換性(エコシステムの混乱防止)の観点から、node_modules 内にある TypeScript ファイルの実行は意図的にブロックされています。ライブラリ作者はこれまで通り JavaScript にトランスパイルしたコードと型定義ファイル(.d.ts)を配布することが推奨されています。
tsconfig.json の推奨設定
ここまでに記載の通り、Node.js の Type Stripping を使うユーザーをサポートするために TypeScript 側にも機能追加が行われています。本稿に記載したオプションは tsconfig.json で指定可能となっており、TypeScript v5.8 以降では以下の設定が推奨されています。
{
"compilerOptions": {
"noEmit": true, // Optional
"target": "esnext",
"module": "nodenext",
// インポートパスの .ts 拡張子を許可し、トランスパイル時に書き換える
"rewriteRelativeImportExtensions": true,
// 消去できない構文(Enumなど)を使ったらエラーにする
"erasableSyntaxOnly": true,
// 型のみのインポートは 'import type' を強制する
"verbatimModuleSyntax": true
}
}
Marco Ippolito 氏の開発秘話
この機能の実装者である Marco Ippolito 氏のブログ記事「The Summer I Shipped Type Stripping」には開発にまつわるエピソードが紹介されているので、最後に少しだけご紹介したいと思います。
ライブラリ名「Amaro」の由来
Node.js 内部で使われている TypeScript パーサー「Amaro」の名前は、イタリア語で「苦い(Bitter)」を意味します。 2024年の夏、Marco 氏はイタリアのアブルッツォ州にある携帯の電波も入らない小さな村に滞在し、Starlink でネットに繋ぎながら開発をしていたそうです。その窓から見えたのが 「Monte Amaro」 という山で、この機能の実装が簡単ではなく「苦い」経験だったことにも掛けているとのことです。
※ このことは GitHub のリポジトリの README.md にも記載されています。
Amaro means "bitter" in Italian. It's a reference to Mount Amaro on whose slopes this package was conceived.
出典:Node.js "amaro/README.md" https://github.com/nodejs/amaro/README.md(参照日:2025年12月5日)
GitHub Sponsors の使い道
この歴史的な機能を実装した Marco 氏ですが、開発は完全に個人の自由時間を使って行われています。その間に得られた GitHub Sponsors からの寄付金は総額で 300 ドルだったそうです。 彼はこのお金で「パン焼き機」を買い、食パンを焼いているとのこと。(本人の公開ブログより翻訳・要約)
なお、Marco 氏が本機能のために作成した PR には 600 を超える
リアクションがつき、本人曰く GitHub 史上最も多くの賛成票を獲得した PR の 1 つとなったそうです。
まとめ
Node.js の v25.2.0 では TypeScript をネイティブに実行するための機能「Type Stripping」が安定版 (Stable) となりました。これにより Node.js から TypeScript コードを直接実行できるようになりますが、一方でプロダクションコードでの利用には多くの制約や注意点があります。そのため TypeScript のプロジェクト全体をトランスパイルせずに Node.js で動かすといったことはまだ時期尚早な印象ですが、小さなタスクをスクリプトとして TypeScript で書くようなユースケースでは非常に便利なのではないかと思います。
今後のアップデートでより改善されていくものと思いますので、引き続きウォッチしていきたいと思います。
参考
- Node.js "Running TypeScript Natively" https://nodejs.org/ja/learn/typescript/run-natively (参照日:2025年12月5日)
- YouTube "JSConf JP 2025|Track C (4:43:05、Running TypeScript natively in Node.js) " https://www.youtube.com/watch?v=jaZp3mc6FU4 (参照日:2025年12月5日)
- Marco Ippolito "Node.js Type Stripping Explained" https://satanacchio.hashnode.dev/everything-you-need-to-know-about-nodejs-type-stripping (参照日:2025年12月5日)
- Marco Ippolito "How a Summer in Abruzzo Helped Bring Type Stripping to Node.js" https://satanacchio.hashnode.dev/the-summer-i-shipped-type-stripping (参照日:2025年12月5日)
※ 記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。