この記事の対象読者
- Node.js + TypeScriptで開発している方
- ts-node / tsx / tsc のビルドステップに疲れている方
- 「Node.jsがTypeScriptをそのまま実行できるらしい」と聞いて気になっている方
この記事で得られること
- Node.jsネイティブTypeScript実行(Type Stripping)の仕組みと使い方がわかる
- ts-nodeからの移行手順が具体的にわかる
- enumが使えない問題の実用的な回避策がわかる
- **「結局どこまで使えるのか」**の判断材料が手に入る
この記事で扱わないこと
- Deno / Bunとの詳細な比較(別記事で扱います)
- フロントエンドのビルドパイプライン(Vite / webpack等)への影響
- TypeScript 5.9の新機能の詳細
1. ts-nodeとの「お別れ」のきっかけ
「またts-nodeのバージョン不整合か...」
Node.jsとTypeScriptで開発していると、誰しも一度はこの呪文を唱えたことがあるんじゃないだろうか。
# こういうの、見覚えない?
$ npx ts-node src/index.ts
TypeError: Unknown file extension ".ts"
tsconfig.jsonをいじり、ts-nodeセクションを追加し、esModuleInteropをtrueにして、moduleResolutionを変えて... 気がつくと30分が溶けている。書きたいのはビジネスロジックであって、ビルド設定ではない。
そんな折、2025年7月31日にリリースされたNode.js v22.18.0 LTSで、ついにType Strippingがデフォルト有効になった。つまり、こうなった:
$ node index.ts # これだけ。嘘じゃない。
ts-nodeもtscも、tsxすらも要らない世界が、もう来ていた。
ここまでで、「おっ」と思った方もいるだろう。次は、この記事で使う用語を整理しておこう。
2. 前提知識の確認
本題に入る前に、この記事で登場する用語を確認しておく。
2.1 Type Strippingとは
Type Stripping(型剥がし)とは、TypeScriptの型注釈をホワイトスペース(空白)に置き換えて除去し、残ったJavaScriptコードをそのまま実行する手法のこと。料理で例えるなら、食材(JavaScript)の包装フィルム(型注釈)をペリッと剥がして、そのまま鍋に入れるイメージだ。
// 元のTypeScript
function greet(name: string): string {
return `Hello, ${name}!`;
}
// Type Stripping後(Node.jsが実行するもの)
function greet(name ) {
return `Hello, ${name}!`;
}
ホワイトスペースで置き換えるため、行番号がそのまま保持される。ソースマップが不要になるという大きなメリットがある。
2.2 Erasable Syntax(消去可能構文)とは
TypeScriptの構文のうち、削除してもランタイムの動作に影響しないものを「Erasable Syntax」と呼ぶ。型注釈、インターフェース、typeキーワードなどがこれにあたる。
逆に、enumやnamespace、クラスのパラメータプロパティは削除するとコードの意味が変わってしまうため、「Non-erasable Syntax」となる。
2.3 Amaro / SWC
Node.jsの内部でType Strippingを実行しているライブラリ。AmaroはNode.js公式のラッパーで、その中身はSWC(Rustで書かれた超高速トランスパイラ)のWebAssembly版。tscの数十倍速い。
これらの用語が押さえられたら、Type Strippingが生まれた背景を見ていこう。
3. Node.jsネイティブTypeScriptが生まれた背景
3.1 「ビルドステップ」という10年来の呪い
2012年にMicrosoftがTypeScriptをリリースして以来、開発者は常に**「書くコード」と「実行されるコード」の間にあるギャップ**と戦ってきた。
| 時代 | 手段 | 痛み |
|---|---|---|
| 2012〜 | tsc手動実行 | 毎回コンパイルが必要 |
| 2016〜 | ts-node登場 | オンザフライ変換だが遅い & 設定地獄 |
| 2019〜 | esbuild / swc | 高速だが外部ツール依存 |
| 2020〜 | Deno / Bun | TS対応だがNode.jsエコシステムと互換性の壁 |
| 2024〜 | Node.js Type Stripping | ゼロ設定、ビルトイン |
3.2 DenoとBunが証明した「未来」
DenoとBunは「ランタイムがTypeScriptを直接理解できる」ことを世界に証明した。しかし、Node.jsには数百万のnpmパッケージとAWSやGCP等のクラウドプラットフォームでのファーストクラスサポートがある。
Node.js本体がTypeScriptを直接実行できるなら、移行コストゼロでこの恩恵を受けられる。それがType Strippingの動機だった。
3.3 バージョンごとの進化
| バージョン | リリース日 | 内容 |
|---|---|---|
| v22.6.0 | 2024年8月 |
--experimental-strip-typesフラグ追加 |
| v22.7.0 | 2024年8月 |
--experimental-transform-types追加(enum等に対応) |
| v23.6.0 | 2025年1月 | Type Strippingがデフォルト有効化(Current版) |
| v22.18.0 | 2025年7月 | LTS版でもデフォルト有効化 |
| v24.3.0 | 2025年 | Experimental警告が消去 |
| v25.2.0 | 2025年11月 | Type Strippingがstable(安定版)に昇格 |
背景がわかったところで、基本的な仕組みを見ていこう。
4. 基本概念と仕組み
4.1 Type Strippingの動作原理
Node.jsがType Strippingで行っていることは、極めてシンプルだ:
-
.tsファイルを読み込む - Amaro(SWCベース)で型注釈をホワイトスペースに置換
- 残ったJavaScriptコードをV8エンジンに渡して実行
型チェックは一切行わない。 string型の引数にnumberを渡してもNode.jsは何も言わずに実行する。型チェックは引き続きtsc --noEmitやIDEに任せる設計だ。
4.2 Erasable vs Non-erasable
| 分類 | 構文 | Type Strippingで使えるか |
|---|---|---|
| Erasable | 型注釈(: string等) |
使える |
| Erasable |
interface / type
|
使える |
| Erasable | ジェネリクス(<T>) |
使える |
| Erasable |
asキャスト |
使える |
| Erasable |
typeインポート |
使える |
| Non-erasable | enum |
使えない |
| Non-erasable | namespace(実行コードあり) |
使えない |
| Non-erasable | クラスのパラメータプロパティ | 使えない |
| Non-erasable | JSX (.tsx) | 使えない |
Non-erasable構文を使うと SyntaxError: TypeScript enum is not supported in strip-only mode が出る。これに遭遇して白目になった人は、私だけではないはずだ。
4.3 ts-nodeとの決定的な違い
| 比較軸 | ts-node | Node.js Type Stripping |
|---|---|---|
| 起動速度 | 約120ms | 約45ms |
| インストール | npm install ts-node typescript |
不要(ビルトイン) |
| 設定ファイル | tsconfig.json必須 | 不要(型チェック用に推奨はされる) |
| enum対応 | 対応 |
--experimental-transform-typesで対応 |
| 型チェック | オプションで実行 | 行わない |
| ソースマップ | 生成する | 不要(行番号が保持される) |
| メモリ使用量 | 大(トランスパイラをメモリにロード) | 小 |
基本概念が理解できたところで、実際にコードを書いて動かしてみよう。
5. 実践:実際に使ってみよう
5.1 環境構築
# Node.jsのバージョン確認(v22.18.0以上が必要)
node -v
# v22.18.0以上なら準備完了。本当にこれだけ。
v22.18.0未満を使っている場合は、--experimental-strip-typesフラグを手動で付ける必要がある:
node --experimental-strip-types index.ts
5.2 最小構成で試す
// hello.ts — これだけのファイルを作る
function greet(name: string): string {
return `Hello, ${name}!`;
}
interface User {
name: string;
age: number;
}
const user: User = { name: "Hiroki", age: 30 };
console.log(greet(user.name));
console.log(`Age: ${user.age}`);
$ node hello.ts
Hello, Hiroki!
Age: 30
以上。ts-nodeのインストールもtsconfig.jsonの作成も不要。
5.3 プロジェクト構成のベストプラクティス
実際のプロジェクトでは、以下の構成を推奨する:
my-project/
├── package.json
├── tsconfig.json # 型チェック用(Node.jsは読まない)
├── src/
│ ├── index.ts
│ ├── utils.ts
│ └── types.ts
└── tests/
└── index.test.ts
package.json
{
"name": "my-project",
"type": "module",
"scripts": {
"start": "node src/index.ts",
"dev": "node --watch src/index.ts",
"typecheck": "tsc --noEmit",
"test": "node --test tests/"
},
"devDependencies": {
"typescript": "^5.8.0"
}
}
"type": "module" を設定すると、.tsファイルがESモジュールとして扱われる。設定しない場合はCommonJSになる。
5.4 環境別の設定ファイル
以下の3種類のtsconfig.jsonを用意した。用途に応じて選択してほしい。
開発環境用(tsconfig.json)— 型チェック専用
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"noEmit": true,
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
本番ビルド用(tsconfig.build.json)— JSファイル出力が必要な場合
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "./dist",
"allowImportingTsExtensions": false,
"rewriteRelativeImportExtensions": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "tests"]
}
CI/テスト用(tsconfig.test.json)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}
5.5 importの書き方(ここが最大のハマりポイント)
ts-nodeからの移行で最も躓くのがimportパスだ。
// ❌ ts-node時代の書き方(動かない)
import { helper } from './utils';
import { MyType } from './types';
// ✅ Node.jsネイティブTS対応の書き方
import { helper } from './utils.ts'; // 拡張子.tsが必須
import type { MyType } from './types.ts'; // 型はtype importが必須
import type を使わずに型をインポートすると、Node.jsはそれを値のインポートとして扱い、ランタイムエラーになる。TypeScript 5.0以降の verbatimModuleSyntax を有効にすれば、エディタが警告してくれる。
5.6 実行結果
上記の構成で実際にプロジェクトを動かしてみよう:
// src/types.ts
export interface Config {
port: number;
host: string;
debug: boolean;
}
export type Environment = 'development' | 'production' | 'test';
// src/utils.ts
import type { Config, Environment } from './types.ts';
export function loadConfig(env: Environment): Config {
const configs: Record<Environment, Config> = {
development: { port: 3000, host: 'localhost', debug: true },
production: { port: 8080, host: '0.0.0.0', debug: false },
test: { port: 3001, host: 'localhost', debug: true },
};
return configs[env];
}
// src/index.ts
import { loadConfig } from './utils.ts';
import type { Environment } from './types.ts';
const env: Environment = (process.env.NODE_ENV as Environment) ?? 'development';
const config = loadConfig(env);
console.log(`Server starting on ${config.host}:${config.port}`);
console.log(`Debug mode: ${config.debug}`);
$ node src/index.ts
Server starting on localhost:3000
Debug mode: true
$ NODE_ENV=production node src/index.ts
Server starting on 0.0.0.0:8080
Debug mode: false
5.7 よくあるエラーと対処法
| エラー | 原因 | 対処法 |
|---|---|---|
SyntaxError: TypeScript enum is not supported in strip-only mode |
enumを使っている |
as constオブジェクトで代替する(後述) |
ERR_MODULE_NOT_FOUND |
importパスに.ts拡張子がない |
import ... from './file.ts'に修正 |
SyntaxError: Unexpected token ':' |
Node.jsのバージョンが古い | v22.18.0以上にアップデート |
TypeError: ... is not a function |
import typeを使うべき場所でimportを使っている |
型のみのインポートにはimport typeを使う |
ExperimentalWarning: Type Stripping is an experimental feature |
v24.3.0未満の警告 |
node --disable-warning=ExperimentalWarningで抑制 |
ERR_UNSUPPORTED_DIR_IMPORT |
ディレクトリインポートしている | ファイルパスを明示的に指定する |
5.8 環境診断スクリプト
問題が発生した場合は、以下のスクリプトで環境を診断できる:
// check-env.ts
import { version } from 'process';
function checkEnvironment(): void {
const issues: string[] = [];
// Node.jsバージョン確認
const [major, minor] = version.slice(1).split('.').map(Number);
if (major < 22 || (major === 22 && minor < 18)) {
issues.push(`Node.js v22.18.0以上が必要です(現在: ${version})`);
issues.push(' → nvm install 22 && nvm use 22');
}
// package.jsonのtypeフィールド確認
try {
const fs = await import('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
if (pkg.type !== 'module') {
issues.push('package.jsonに "type": "module" がありません(ESM推奨)');
}
} catch {
issues.push('package.jsonが見つかりません');
}
// TypeScriptバージョン確認
try {
const { execSync } = await import('child_process');
const tsVersion = execSync('npx tsc --version', { encoding: 'utf-8' }).trim();
console.log(`TypeScript: ${tsVersion}`);
// 5.8以上を推奨
const tsMinor = parseInt(tsVersion.split(' ')[1]?.split('.')[1] ?? '0');
if (tsMinor < 8) {
issues.push('TypeScript 5.8以上を推奨します(erasableSyntaxOnlyサポート)');
}
} catch {
issues.push('TypeScriptがインストールされていません(型チェック用に推奨)');
}
if (issues.length > 0) {
console.log('❌ 問題が見つかりました:');
for (const issue of issues) {
console.log(` - ${issue}`);
}
} else {
console.log('✅ 環境は正常です — node file.ts で実行できます');
}
}
checkEnvironment();
$ node check-env.ts
TypeScript: Version 5.9.0
✅ 環境は正常です — node file.ts で実行できます
実装方法がわかったので、次は具体的なユースケースを見ていこう。
6. ユースケース別ガイド
6.1 ユースケース1: CLIスクリプト / ワンショットタスク
想定読者: 日常的にPythonやBashで書いているようなスクリプトをTypeScriptで書きたい人
推奨構成: 単一ファイルで完結。tsconfig.jsonすら不要。
サンプルコード:
// cleanup.ts — 古いログファイルを削除するスクリプト
import { readdir, stat, unlink } from 'fs/promises';
import { join } from 'path';
const LOG_DIR = './logs';
const MAX_AGE_DAYS = 30;
async function cleanupOldLogs(dir: string, maxAgeDays: number): Promise<number> {
const files = await readdir(dir);
const now = Date.now();
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
let deleted = 0;
for (const file of files) {
if (!file.endsWith('.log')) continue;
const filepath = join(dir, file);
const fileStat = await stat(filepath);
if (now - fileStat.mtimeMs > maxAgeMs) {
await unlink(filepath);
console.log(`Deleted: ${file}`);
deleted++;
}
}
return deleted;
}
const count = await cleanupOldLogs(LOG_DIR, MAX_AGE_DAYS);
console.log(`Done. ${count} files deleted.`);
$ node cleanup.ts
Deleted: app-2026-01-15.log
Deleted: app-2026-01-20.log
Done. 2 files deleted.
6.2 ユースケース2: Express APIサーバー(開発環境)
想定読者: ExpressでAPIを開発していて、開発時のビルドステップを省きたい人
推奨構成: node --watchとの組み合わせでホットリロード開発
サンプルコード:
// src/server.ts
import express from 'express';
import type { Request, Response } from 'express';
interface Todo {
id: number;
title: string;
completed: boolean;
}
const app = express();
app.use(express.json());
const todos: Todo[] = [];
let nextId = 1;
app.get('/todos', (_req: Request, res: Response) => {
res.json(todos);
});
app.post('/todos', (req: Request, res: Response) => {
const todo: Todo = {
id: nextId++,
title: req.body.title,
completed: false,
};
todos.push(todo);
res.status(201).json(todo);
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
# ホットリロード付きで起動
$ node --watch src/server.ts
Server running on http://localhost:3000
node --watchはNode.js v18.11.0以降で利用可能。nodemonも不要になる。
6.3 ユースケース3: enumの代替パターン(最重要)
想定読者: 既存プロジェクトでenumを多用していて移行に悩んでいる人
推奨構成: as constオブジェクト + 型ユーティリティ
enumが使えないのはNode.jsネイティブTS最大の制約だ。しかし、as constパターンで実用上はほぼ完全に代替できる。
サンプルコード:
// ❌ これは動かない(Non-erasable構文)
// enum Direction {
// Up = 'UP',
// Down = 'DOWN',
// Left = 'LEFT',
// Right = 'RIGHT',
// }
// ✅ as const パターンで代替
const Direction = {
Up: 'UP',
Down: 'DOWN',
Left: 'LEFT',
Right: 'RIGHT',
} as const;
// 型を自動生成(enum相当の型安全性を確保)
type Direction = (typeof Direction)[keyof typeof Direction];
// → 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
function move(direction: Direction): void {
console.log(`Moving ${direction}`);
}
move(Direction.Up); // ✅ OK
move('UP'); // ✅ OK(リテラル型として一致)
// move('DIAGONAL'); // ❌ コンパイルエラー(型チェック時)
// ❌ パラメータプロパティも動かない
// class User {
// constructor(private name: string) {}
// }
// ✅ 通常のプロパティ宣言で代替
class User {
private name: string;
constructor(name: string) {
this.name = name;
}
}
TypeScript 5.8のerasableSyntaxOnlyオプションを有効にしておくと、Non-erasable構文を使った時点でエディタが警告してくれるので、ランタイムエラーの前に気付ける。
ユースケースを把握できたところで、この先の学習パスを確認しよう。
7. 学習ロードマップ
この記事を読んだ後、次のステップとして以下をおすすめする。
初級者向け(まずはここから)
- Node.js公式: Running TypeScript Natively — 公式チュートリアル
- 手元の小さなスクリプトを1つ、
.tsにリネームしてnode script.tsで動かしてみる -
tsconfig.jsonにerasableSyntaxOnly: trueを追加して、非対応構文がないかチェック
中級者向け(実践に進む)
- 既存プロジェクトのts-nodeを
nodeコマンドに段階的に置き換える - enumを
as constパターンに移行する - Node.js公式API: TypeScript — 制約事項の詳細を把握
- CI/CDパイプラインで
tsc --noEmitの型チェックを組み込む
上級者向け(さらに深く)
- TC39 Types as Comments Proposal — ブラウザでもType Strippingが実現する未来
- Amaro (nodejs/amaro) — Node.js内部のType Strippingエンジン
-
--experimental-transform-typesフラグで、enum/namespaceの変換動作を検証する
8. まとめ
この記事では、Node.jsネイティブTypeScript実行(Type Stripping)について以下を解説した:
-
v22.18.0以降、
node file.tsだけでTypeScriptが実行できる — ts-node / tsx / tsc のビルドステップは開発時に不要 - 仕組みは「型をホワイトスペースに置換するだけ」 — 超高速で、ソースマップも不要
-
enum / namespace / パラメータプロパティは使えない —
as constパターンで代替可能 -
型チェックはNode.jsの仕事ではない —
tsc --noEmitをCIに組み込むのがベストプラクティス
私の所感
正直なところ、最初は「enum使えないの? じゃあ無理じゃん」と思った。しかし実際に移行してみると、enumをas constに書き換える作業は機械的で、半日もかからなかった。
それ以上に、開発サーバーの起動が一瞬になる体験のインパクトが大きい。node --watch src/index.tsで、ファイルを保存した瞬間にサーバーが再起動される。ts-node時代の「あの数秒の待ち」がゼロになる感覚は、一度味わうと戻れない。
Node.jsは「TypeScriptのランタイム」ではなく「JavaScriptのランタイム」であり続ける、というスタンスが設計から明確に見える。型チェックという重い仕事はtscに任せ、実行に必要な最小限の処理だけを引き受ける。この割り切りが、結果として最もシンプルで高速な開発体験を生み出している。
2026年、TypeScript開発の「普通」が変わりつつある。まだ試していないなら、今日の5分で体験してみてほしい。
参考文献
- Node.js公式ドキュメント: Modules: TypeScript
- Node.js公式ガイド: Running TypeScript Natively
- Marco Ippolito: Everything You Need to Know About Node.js Type Stripping
- Dr. Axel Rauschmayer: Node's new built-in support for TypeScript
- TypeScript 5.8: erasableSyntaxOnly
- TC39 Proposal: Type Annotations (Types as Comments)
この記事が役に立ったら、いいねとストックをお願いします。