Node.js 23.6以降でTypeScriptを直接実行する実践ガイド
Node.jsがTypeScriptファイルを直接実行する機能は、開発ワークフローを大幅に簡素化します。本記事では、外部ローダーなしでTypeScriptをNode.jsで実行するための具体的な手順、考慮事項、そして開発環境のセットアップ簡素化とパフォーマンス向上を実現する方法を解説します。
解決する課題
従来のTypeScriptプロジェクトでは、開発時も本番稼働時も、tscやts-node、tsxといったトランスパイラ/ローダーが必要でした。これにより、ビルドステップが必須となり、開発サイクルの遅延や依存ライブラリの増加といった課題がありました。Node.jsのネイティブTypeScript実行機能は、これらの課題を解決し、よりシンプルで高速な開発体験を提供します。
前提・環境
本記事で解説する機能は、以下のNode.jsおよびTypeScriptのバージョンで利用可能です。
-
Node.js:
-
v22.18.0以降、またはv23.6.0以降:
--experimental-strip-typesフラグなしでTypeScriptファイルを直接実行できます。 -
v22.7.0以降:
--experimental-strip-typesフラグが必要です。
-
v22.18.0以降、またはv23.6.0以降:
-
TypeScript: 5.8以降を推奨します。特に
"erasableSyntaxOnly": trueコンパイラフラグを活用するためです。
本記事ではNode.js v23.6.0以降を前提に説明を進めます。
Node.jsのネイティブTypeScript実行の仕組み
Node.jsがTypeScriptを直接実行する際、内部では「型ストリッピング(Type Stripping)」という軽量なプロセスが実行されます。これは、TypeScriptの型アノテーション、インターフェース、型エイリアス、import typeなどの型情報のみの構文を実行時に削除し、残りの有効なJavaScriptコードを直接実行する仕組みです。
この機能は、内部的に@swc/wasm-typescriptのWebAssemblyポートであるAmaroというライブラリを使用しています。これにより、ts-nodeのような外部ローダーを導入することなく、Node.js自体がTypeScriptを解釈・実行できるようになりました。
重要な点として、このプロセスは型チェックを実行しません。型チェックは別途tscコマンドで実施する必要があります。
実装手順
Node.jsでTypeScriptを直接実行するプロジェクトのセットアップ手順を説明します。
1. プロジェクトの初期化
まず、新しいプロジェクトを作成し、TypeScriptを開発依存としてインストールします。TypeScript 5.8以降を推奨します。
mkdir my-ts-app
cd my-ts-app
npm init -y
npm install --save-dev typescript@^5.8.0
2. tsconfig.jsonの作成(推奨)
Node.jsのTypeScriptローダーはtsconfig.jsonを必須としません。しかし、IDEのサポート、型チェック、およびNode.jsのモジュール解決と整合性のある開発体験のために、tsconfig.jsonを作成することを強く推奨します。
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"erasableSyntaxOnly": true, // TypeScript 5.8以降で推奨。型ストリッピングと互換性のない構文を検出
"verbatimModuleSyntax": true // import/export構文の変換を抑制し、Node.jsのESM解決と整合性を高める
},
"include": ["src/**/*"]
}
-
"erasableSyntaxOnly": true: TypeScript 5.8で導入されたフラグです。型ストリッピングによって削除されるべきではない構文(例:enum、パラメータープロパティ)が使用された場合にエラーを発生させます。これにより、型ストリッピングの制約を開発段階で把握できます。 -
"verbatimModuleSyntax": true:importやexportの構文がNode.jsのESM解決と一致するように強制されます。これにより、TypeScriptコンパイラがモジュール解決を勝手に変更するのを防ぎます。
3. TypeScriptファイルの作成
src/index.tsファイルを作成し、TypeScriptコードを記述します。
// src/index.ts
type Status = "pending" | "success" | "error";
function processItem(id: number, status: Status): string {
return `Item ${id} is ${status}`;
}
const result = processItem(42, "success");
console.log(result);
// erasableSyntaxOnlyフラグとの互換性を考慮し、パラメータープロパティは使用しない
// (例: constructor(public id: number) は erasableSyntaxOnly: true でエラーになる)
class Item {
id: number;
status: Status;
constructor(id: number, status: Status) {
this.id = id;
this.status = status;
}
toString(): string {
return `Item(${this.id}, ${this.status})`;
}
}
const item = new Item(123, "pending");
console.log(item.toString());
4. package.jsonスクリプトの追加
package.jsonに実行スクリプトを追加します。type: "module"を設定することで、Node.jsがES Modulesとしてファイルを扱います。これはtsconfig.jsonの"module": "NodeNext"と整合性が取れます。
// package.json
{
"name": "my-ts-app",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"type": "module", // ES Modulesとして実行する場合に推奨
"scripts": {
"start": "node src/index.ts",
"dev": "node --watch src/index.ts",
"type-check": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.8.0"
}
}
5. TypeScriptコードの実行
定義したスクリプトを実行します。
npm start
Node.js v22.18.0またはv23.6.0以降を使用している場合、--experimental-strip-types フラグは不要です。それ以前のバージョン(v22.7.0以降)では node --experimental-strip-types src/index.ts が必要です。
6. 開発中のホットリロード
Node.jsの組み込み--watchフラグを利用して、ファイルの変更を検知し自動で再実行できます。
npm run dev
7. 型チェックのみの実行
Node.jsのネイティブ実行は型チェックを行わないため、型安全性を確保するために別途型チェックを実行します。
npm run type-check
このコマンドは、tsconfig.jsonに基づいて型チェックを実行しますが、JavaScriptファイルの出力は行いません。CI/CDパイプラインやコミット前のフックとして組み込むことが推奨されます。
よくあるエラー・ハマりどころと回避策
1. 型チェックが行われない
- 問題: Node.jsのネイティブ実行は型ストリッピングのみを行い、型チェックは実行しません。そのため、型エラーがあっても実行時にJavaScriptとして動作し、問題が発覚しにくい場合があります。
-
回避策:
- 開発中にIDE(VS Codeなど)の型チェック機能を活用します。
- コミット前やCI/CDパイプラインで
tsc --noEmitを実行し、明示的に型チェックを行います。package.jsonに"type-check": "tsc --noEmit"スクリプトを追加し、定期的に実行することを推奨します。
2. サポートされていないTypeScript構文
-
問題: 型ストリッピングは、実行時のJavaScriptコードに影響を与えずに削除できるTypeScript構文のみをサポートします。具体的には、
enum、パラメータープロパティ(例:constructor(public name: string))、ランタイムコードを持つnamespaceやmodule、非ECMAScriptのimport =やexport =などの構文はサポートされません。これらの構文を使用すると、実行時に予期せぬ動作やエラーが発生する可能性があります。 -
回避策:
- TypeScript 5.8以降で
tsconfig.jsonに"erasableSyntaxOnly": trueを設定し、コンパイル時に互換性のない構文の使用を検出します。これにより、開発段階でエラーとして報告されます。 - これらの機能が必要な場合は、
ts-nodeやtsxのようなトランスパイラを使用するか、別のトランスパイルステップを導入することを検討します。
- TypeScript 5.8以降で
3. モジュール解決の問題(パスエイリアス)
-
問題:
tsconfig.jsonで定義されたパスエイリアス(例:"paths": { "@app/*": ["./src/*"] })は、Node.jsのネイティブ実行では直接機能しません。Node.jsはtsconfig.jsonを読み込まないため、エイリアスを解決できません。 -
回避策:
-
package.jsonのimportsフィールドにNode.jsのサブパスパターンを定義することで、同様の効果を得ることができます。そして、コード内のインポート文を// package.json { "type": "module", // ES Modules環境でのみ有効 "imports": { "#app/*": "./src/*" } }@app/lib/somethingから#app/lib/somethingのように更新する必要があります。 -
importsフィールドはES Modules環境でのみ機能します。CommonJS環境では別の解決策(例:module-aliasライブラリ)が必要です。
-
4. Node.jsのエラーハンドリングとTypeScriptの型定義の不一致
-
問題: Node.jsの
Errorオブジェクトにはcodeプロパティなど、TypeScriptの標準Errorインターフェースにはないプロパティが存在することがあります。これにより、catch (e)ブロック内でe.codeにアクセスしようとすると型エラーが発生します。 -
回避策:
- エラーオブジェクトが特定の型であることを確認するための型ガード(Type Guard)を実装します。
function isNodeError(error: unknown): error is NodeJS.ErrnoException { return typeof error === 'object' && error !== null && 'code' in error; } try { // ... } catch (e) { if (isNodeError(e)) { console.error(`Error code: ${e.code}`); } else { console.error(e); } } -
NodeJS.ErrnoException型を直接使用するか、カスタムエラークラスを定義して、エラーの型安全性を確保します。
- エラーオブジェクトが特定の型であることを確認するための型ガード(Type Guard)を実装します。
設計上のトレードオフ・ベストプラクティス
Node.jsのネイティブTypeScript実行は強力な機能ですが、その利用にはトレードオフが存在し、適切なベストプラクティスを適用する必要があります。
トレードオフ
-
開発速度 vs. 型安全性: ネイティブ実行はビルドステップを不要にし、開発サイクルを高速化しますが、実行時に型チェックを行わないため、型安全性の保証は
tsc --noEmitなどの別途の型チェックに依存します。 -
シンプルさ vs. 機能の制限:
ts-nodeなどのツールに比べて設定がシンプルになりますが、enumやパラメータープロパティなど、型ストリッピングでサポートされないTypeScriptの高度な機能には制限があります。 -
開発環境 vs. 本番環境: ネイティブ実行は開発時の高速なフィードバックに適していますが、型ストリッピングはまだ実験的な機能であるため、本番環境での使用は推奨されません。本番環境では、宣言ファイルの生成や古いランタイムのターゲット指定のために、引き続き
tscによるコンパイルが必要となる場合があります。
ベストプラクティス
-
開発時の高速なフィードバック: 開発中はNode.jsのネイティブTypeScript実行を積極的に利用し、
--watchフラグと組み合わせて高速な開発サイクルを実現します。 -
型チェックの徹底: 開発中もIDEの型チェックを最大限に活用し、コミット前やCI/CDパイプラインで
tsc --noEmitを実行して、型安全性を確保します。 -
erasableSyntaxOnlyの活用: TypeScript 5.8以降を使用している場合は、tsconfig.jsonに"erasableSyntaxOnly": trueを設定し、型ストリッピングと互換性のない構文の使用を早期に検出します。 -
本番環境でのビルド: 本番環境へのデプロイ時には、引き続き
tscなどのコンパイラを使用してJavaScriptにトランスパイルし、最適化されたコードを生成します。これにより、ランタイムの依存性を減らし、パフォーマンスと安定性を向上させます。 -
モジュール解決の統一: パスエイリアスを使用する場合は、
tsconfig.jsonとpackage.jsonのimportsフィールドの両方で設定を統一し、Node.jsのモジュール解決とTypeScriptの型解決が一致するようにします。 - エラーハンドリングの型安全化: Node.jsのエラーを扱う際は、型ガードやカスタムエラークラスを活用して、エラーオブジェクトの型安全性を確保します。
まとめ
Node.jsのネイティブTypeScript実行機能は、開発ワークフローを大幅に簡素化し、ビルドステップの排除と開発サイクルの高速化を実現します。ts-nodeのような外部ローダーなしでTypeScriptコードを実行できるようになったことは、Node.jsエコシステムにおける大きな進歩です。
本記事で示したように、適切なtsconfig.jsonの設定、package.jsonスクリプトの活用、そして型チェックの分離といったベストプラクティスを適用することで、この新機能を最大限に活用できます。特に、erasableSyntaxOnlyフラグやimportsフィールドを活用することで、ネイティブ実行の制約を管理しつつ、生産性の高い開発環境を構築可能です。
本番環境では引き続きトランスパイルが必要となる場面が多いですが、開発環境においてはNode.jsのネイティブTypeScript実行が標準的な手法となる可能性を秘めています。