Node.jsネイティブTypeScript:コンパイラなしで.tsファイルを実行する完全ガイド
10年以上にわたり、Node.jsでTypeScriptを実行するということは「まずコンパイル、それから実行」を意味していました。tscを使おうが、ts-nodeを使おうが、tsxを使おうが、.tsファイルから実行までの間に必ず中間ステップが存在していたんです。その時代が終わりを迎えつつあります。
Node.js 22から始まり、Node.js 25で安定版となったネイティブTypeScriptサポートにより、TypeScriptファイルを直接実行できるようになりました:
node app.ts
ビルドステップは不要です。tsconfig.jsonも必須ではありません。外部依存パッケージも不要です。Node.jsとTypeScriptコードだけで動きます。
これは実験的な機能でもニッチな仕組みでもありません。すべてのTypeScript開発者に影響する根本的な変化です。この記事では、その仕組み、できること・できないこと、そして実際のプロジェクトでの導入方法を詳しく解説します。
動作原理:型チェックではなく型ストリッピング
Node.jsのアプローチの核心は、驚くほどシンプルです:型を取り除き、残ったJavaScriptを実行する。
Node.jsはTypeScriptをコンパイルしません。型チェックも行いません。構文の変換もしません。文字通り型アノテーションを除去して、残りを通常のJavaScriptとして実行するだけです。
次のTypeScriptファイルを考えてみましょう:
// app.ts interface User { name: string; age: number; } function greet(user: User): string { return `Hello, ${user.name}! You are ${user.age} years old.`; } const user: User = { name: 'Alice', age: 30 }; console.log(greet(user));
Node.jsはこれをおおよそ次のように変換します:
function greet(user) { return `Hello, ${user.name}! You are ${user.age} years old.`; } const user = { name: 'Alice', age: 30 }; console.log(greet(user));
interface User宣言は消えます。: User型アノテーションは除去されます。: string戻り値の型は削除されます。残るのは、V8がそのまま実行できるクリーンなJavaScriptだけ。
これを**「消去可能な構文(erasable syntax)」**と呼びます。ランタイムの動作を変えることなく除去できるTypeScript構文のことです。開発者が日常的に使うTypeScript機能の大部分がこれに該当するんですよね。
内部実装:AmaroとSWC
中身を見てみると、Node.jsはAmaroというライブラリで型ストリッピングを処理しています。Amaroは@swc/wasm-typescript(SWCのTypeScriptパーサーのWebAssemblyビルド)の薄いラッパーなんです。
パイプラインはこんな感じです:
your-file.ts → Amaro (SWC WASM) → 型除去済みJS → V8実行
このアーキテクチャで押さえておくべきポイントが3つあります:
-
速いんです。 SWCはRustで書かれ、WebAssemblyにコンパイルされています。パースと除去だけなので、
tscの完全なコンパイルより桁違いに速い。型の解決、制約の検証、宣言ファイルの生成はやらないからです。 -
組み込みです。 AmaroはNode.js自体に同梱されています。
npm installは不要。node_modulesを汚しません。ランタイムの一部なんです。 -
意図的に制限されています。 デフォルトモードではSWCによるストリッピングのみ(完全なコンパイルじゃない)なので、消去可能なTypeScript構文だけがサポートされます。
タイムライン:実験から安定まで
ネイティブTypeScriptサポートへの道のりを整理します:
| バージョン | マイルストーン |
|---|---|
| Node.js 22.6.0(2024年7月) | --experimental-strip-typesフラグ導入 |
| Node.js 22.7.0(2024年8月) | enum対応の--experimental-transform-types追加 |
| Node.js 23.x(2024年末) | 改善と広範なテスト |
| Node.js 22.18.0 / 23.6.0(2025年) | 型ストリッピングがデフォルト有効(フラグ不要) |
| Node.js 25.2.0(2026年) | 安定版リリース、実験的警告の削除 |
2026年初頭時点で、Node.js 22.18+または最新のNode.jsを使用していれば、型ストリッピングは設定不要でそのまま動作します。
動作するもの:消去可能なTypeScript構文
以下のTypeScript機能は、ランタイムの動作に影響を与えずにきれいに除去できるため、完全に動作します:
型アノテーション
// これらはすべてストリッピングされます const name: string = 'hello'; function add(a: number, b: number): number { return a + b; } const items: Array<string> = ['a', 'b', 'c'];
インターフェースと型エイリアス
interface Config { host: string; port: number; debug?: boolean; } type DatabaseURL = `postgres://${string}`; // ランタイムには存在しません — 完全に消去されます
ジェネリクス
function identity<T>(value: T): T { return value; } class Container<T> { constructor(private value: T) {} get(): T { return this.value; } }
型アサーションとasキャスト
const input = document.getElementById('name') as HTMLInputElement; const data = JSON.parse(body) as ApiResponse;
satisfies演算子
const config = { host: 'localhost', port: 3000, } satisfies Config;
ユーティリティ型
type ReadonlyUser = Readonly<User>; type PartialConfig = Partial<Config>; type UserKeys = keyof User; // 型ストリッピング時にすべて消去されます
動作しないもの:消去不可能な構文
ここからが少し複雑になります。一部のTypeScript機能はランタイムコードを生成します。除去すると動作が変わるため、単純にストリッピングできません。これらは「消去不可能(non-erasable)」な機能と呼ばれます。
Enum(クラシックEnum)
// ❌ 型ストリッピングのみではランタイムエラー enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT', }
Enumはランタイム時にJavaScriptオブジェクトを生成します。enumキーワードをストリッピングすると無効な構文が残ります。Enumを使用するには--experimental-transform-typesフラグが必要です:
node --experimental-transform-types app.ts
より良い代替手段: enumの代わりにas constオブジェクトを使いましょう:
// ✅ 通常の型ストリッピングで動作します const Direction = { Up: 'UP', Down: 'DOWN', Left: 'LEFT', Right: 'RIGHT', } as const; type Direction = typeof Direction[keyof typeof Direction];
パラメータプロパティ
// ❌ --experimental-transform-typesが必要 class User { constructor(public name: string, private age: number) {} }
パラメータプロパティ(コンストラクタパラメータのpublic、private、protected、readonly)は代入コードを生成します。public name: stringという省略形はコンストラクタ本体でthis.name = name;になります。
代替手段:
// ✅ 通常の型ストリッピングで動作します class User { name: string; // 型アノテーション — ストリッピングされる private _age: number; constructor(name: string, age: number) { this.name = name; this._age = age; } }
レガシーデコレータとemitDecoratorMetadata
// ❌ サポートされていません — ランタイムメタデータを生成します @Controller('/users') class UserController { @Get('/:id') getUser(@Param('id') id: string) { ... } }
レガシー(実験的)デコレータとemitDecoratorMetadataはランタイム時にリフレクションメタデータを出力します。NestJS、TypeORM、Angularなどのフレームワークでよく使われます。
注意: TC39 Stage 3デコレータ(モダンな標準)はJavaScript機能であり、Node.jsのJavaScriptエンジンが別途処理します。
ランタイムマージを伴うNamespace
// ❌ サポートされていません — namespaceはIIFEを生成します namespace Validation { export function isEmail(str: string): boolean { return str.includes('@'); } }
代替手段: ESモジュールを使いましょう:
// validation.ts export function isEmail(str: string): boolean { return str.includes('@'); }
重要な制約と注意点
型チェックは行われません
最も重要なポイントです:Node.jsはコードの型をチェックしません。型エラーがあっても:
const name: number = "hello"; // 型エラー!
Node.jsは: numberアノテーションを何の問題もなく除去してコードを実行します。型が正しいかどうかには一切関心がありません。それは引き続きtscの仕事です。
ワークフローは次のようになります:
# 開発:そのまま実行 node app.ts # CI/CD:型チェックは別途 npx tsc --noEmit
実はこれは大きな速度向上です。開発中は型チェックを完全にスキップして即座に実行できます。型チェックはエディタ(TypeScript言語サーバー経由)とCIで行われます。
Import指定子:.ts vs .js
移行時に最もややこしい問題の一つです。ES Modulesを使用する場合、Node.jsはインポートに明示的なファイル拡張子を要求します:
// ❌ あいまい — Node.jsはどのファイルかわかりません import { greet } from './utils'; // ✅ 明示的な.ts拡張子 import { greet } from './utils.ts';
解決策: TypeScript 5.7+でrewriteRelativeImportExtensionsコンパイラオプションが導入されました:
{ "compilerOptions": { "rewriteRelativeImportExtensions": true } }
これによりtscは.tsインポート指定子を受け入れ、出力で.jsに書き換えるようになります。
tsconfig.jsonのパスエイリアスは使えません
// ❌ Node.jsはtsconfig.jsonを読みません import { db } from '@/database';
Node.jsはtsconfig.jsonを読み込んだり処理したりしません。パスエイリアスはコンパイル時の機能です。代替手段としては:
package.jsonのNode.js subpath imports:
{ "imports": { "#src/*": "./src/*" } }
import { db } from '#src/database.ts';
- 相対パスimport(ほとんどの場合で最もシンプルなアプローチ)
実践的な移行:ts-nodeからネイティブへ
一般的なts-nodeセットアップからの実際の移行方法を見ていきましょう。
移行前:ts-nodeセットアップ
// package.json { "scripts": { "dev": "ts-node --esm src/index.ts", "start": "node dist/index.js", "build": "tsc" }, "devDependencies": { "typescript": "^5.5.0", "ts-node": "^10.9.0", "@types/node": "^22.0.0" } }
移行後:Node.jsネイティブTypeScript
// package.json { "scripts": { "dev": "node --watch src/index.ts", "start": "node src/index.ts", "typecheck": "tsc --noEmit", "build": "tsc" }, "devDependencies": { "typescript": "^5.7.0", "@types/node": "^22.0.0" } }
変更点を確認しましょう:
ts-nodeが消えました。devDependenciesから完全に削除されています。devスクリプトがただのnodeになりました。--watchと組み合わせて組み込みのウォッチモードを使用します。startが.tsを直接実行します。 開発のためにdist/へコンパイルする必要がなくなりました。typecheckが分離されました。 型チェックが明示的でオプショナルなステップになります。buildは引き続きtscを使います。 宣言ファイルが必要な場合や古いランタイム向けの場合です。
移行手順
-
Node.jsをアップデート — 22.18+(または警告なしの安定版25+)
-
ts-nodeとtsxを削除:
npm uninstall ts-node tsx
- import拡張子を
.tsに更新:
// 移行前 import { db } from './database.js'; // 移行後 import { db } from './database.ts';
-
enumを
as constオブジェクトに置き換え(または--experimental-transform-typesを使用) -
tsconfigで
erasableSyntaxOnlyを有効化(TypeScript 5.8+):
{ "compilerOptions": { "erasableSyntaxOnly": true } }
このフラグはtscに対して、消去不可能な構文(enum、パラメータプロパティ、namespace)をコンパイル時に拒否するよう指示します。Node.jsの型ストリッピングと互換性のない機能を誤って使用した場合、TypeScriptがランタイム前に検出してくれます。
-
package.jsonのスクリプトを更新 -
CIに
typecheckを追加:
# GitHub Actions - name: Type Check run: npx tsc --noEmit
今すぐ導入すべきですか?
判断マトリクスを整理しました:
| シナリオ | 推奨事項 |
|---|---|
| 新規プロジェクト、最新Node.js | ✅ ネイティブTypeScriptを使う |
| CLIツールやスクリプト | ✅ 完璧なユースケース — ビルドステップゼロ |
| 開発/プロトタイピング | ✅ 最速のイテレーション |
| APIサーバー(Express, Fastify) | ✅ 問題なく動作、CIにtscを追加 |
| NestJS / TypeORM(デコレータ) | ⚠️ 待機 — レガシーデコレータ未サポート |
| 宣言ファイルが必要なライブラリ | ⚠️ .d.ts生成のためにtscが依然必要 |
| 古いNode.js向けプロダクションビルド | ❌ コンパイルのためにtscを維持 |
推奨ハイブリッドワークフロー
2026年のほとんどのプロジェクトで、最適なワークフローは次の通りです:
開発: node --watch app.ts (即時実行、ビルド不要)
エディタ: TypeScript LSP (リアルタイム型チェック)
CI: tsc --noEmit (厳密な型検証)
本番: node app.ts (直接実行)
エコシステムへの影響
Node.jsのネイティブTypeScriptサポートは、エコシステム全体に波及効果をもたらしています:
必要性が低下するツール:
ts-node— 最も直接的な代替。開発用としてはほぼ不要にtsx— ts-nodeのより高速な代替手段だったが、同じ状況esbuild/swc(開発用トランスパイラとして) — Node.jsがこれを処理するように
依然として不可欠なツール:
tsc— 型チェック、宣言ファイル生成、古い環境をターゲットにする場合- バンドラー(Vite, webpack, Rollup) — ブラウザコード、ツリーシェイキング、最適化用
@swc/core/esbuild— 完全な変換が必要なプロダクションビルドパイプライン用
適応しているフレームワーク:
- DenoとBunはすでにネイティブTypeScriptサポートを備えていました。Node.jsの参入で競争が平準化された形です。
- NestJSは移行パスとしてTC39デコレータ(型変換不要)の採用を検討中です。
- ExpressとFastifyはネイティブ型ストリッピングと完璧に互換性があります。何も変える必要がありません。
今後の展望
Node.jsのネイティブTypeScriptサポートは、もっと大きなトレンドを映し出しています。TypeScriptとJavaScriptの境界線がどんどん曖昧になっているということ。TypeScriptはもはや「JSにコンパイルする言語」じゃないんです。ランタイムがネイティブに理解するJavaScriptの方言になりつつあります。
TypeScriptチームもこの流れを加速させています。TypeScript 5.8で--erasableSyntaxOnlyフラグを追加して、コンパイル時にNode.js互換性を強制できるようにしました。TypeScript 6.0 Betaは2026年2月にリリース済み。そして最も注目すべきは**TypeScript 7.0(Project Corsa)**で、コンパイラと言語サービスをGoで完全に書き直すプロジェクトです。10倍のパフォーマンス改善を目指しています。2026年半ばのリリース予定で、これが実現したらtsc --noEmitがめちゃくちゃ速くなって、「開発中の型チェックをスキップする」という議論自体が意味をなさなくなるかもしれません。
一方、TC39 Type Annotations提案(Stage 1)は、型アノテーションをJavaScript仕様の一部にしようとしています。この提案が進めば、ブラウザもランタイムもネイティブに型アノテーションを無視するようになります。Node.jsが今Amaroでやっていることと全く同じですね。
「TypeScriptをどうコンパイルするか?」ではなく「なぜコンパイルする必要があるのか?」と問う世界に向かっています。
ほとんどのアプリケーションにおいて、答えはどんどん明確になっています。必要ないんです。
まとめ
Node.jsのネイティブTypeScriptサポートは、単なる便利機能じゃありません。パラダイムシフトなんです。開発ループが編集 → コンパイル → 実行から編集 → 実行に短縮されます。依存関係ツリーが軽くなる。ビルド設定で頭を悩ませることがなくなる。
2026年に新しいNode.jsプロジェクトを始めるなら、開発用のTypeScriptコンパイルパイプラインを構築する理由はありません。.tsファイルを書いてnodeで実行するだけです。tscはCIでの型チェックと、宣言ファイルが必要なケースだけに残しておけばいいでしょう。
Node.jsにおけるTypeScriptの未来は、より良いコンパイラの話じゃないんです。コンパイラ自体が要らなくなる、という話なんです。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう