node app.ts のように TypeScript ファイルを Node で直接実行しようとして、次のエラーで止まることがあります。
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /path/to/app.ts
.mts / .cts でも同じ表示になります。また package.json に "type": "module" が無い環境では、このエラーではなく型注釈で構文エラーになることがあります(例: SyntaxError: Missing initializer in const declaration や Unexpected token)。どちらも「Node が .ts をそのまま扱えない」という同じ原因です。
Node が .ts という拡張子を読み込み方の分からないファイルとして扱い、実行を拒否しています。直し方は Node のバージョンで変わるので、まず node -v を確認してください。
Node のバージョンで対応が三分される
Node は .ts を実行する機能(型を取り除いてから走らせる「type stripping」)を段階的に取り込んできました。動いている Node がどの段階かで、やることが変わります。
| Node のバージョン | node app.ts の扱い | 対応 |
|---|---|---|
| 22.6 〜 22.17 | フラグなしでは拒否される | --experimental-strip-types を付ける |
| 22.18 以上 / 23.6 以上 / 24 | erasable な構文(型注釈のみ等)はそのまま実行できる | 型注釈だけなら対応不要 |
| 22.5 以前(18 / 20 など) | type stripping 自体が無い | Node を上げる、または事前に .js へコンパイルする |
同じ node app.ts でも、新しい Node では通るのに、少し前の Node ではフラグが要る/そもそも動かない、という差が出ます。手元では動くのに CI やチームの古い環境で ERR_UNKNOWN_FILE_EXTENSION が出るときは、まずこの版差を疑ってください。
再現(最小構成)
型注釈を含むだけの TypeScript を用意します。
// app.ts
const message: string = 'hello from TypeScript';
console.log(message);
package.json に "type": "module" があると、Node は .ts を ES Module として解決しようとし、拡張子を扱えずに ERR_UNKNOWN_FILE_EXTENSION を投げます。
{
"type": "module"
}
Node 22.6 でこの構成のまま node app.ts を実行すると、終了コードは 1 になります。
解決:--experimental-strip-types を付ける(Node 22.6〜22.17)
ファイルもコマンドの対象も変えず、フラグだけを足します。
node --experimental-strip-types app.ts
これで Node が .ts を認識し、型注釈を取り除いてから実行します。終了コードは 0 になります。同じ app.ts を、フラグを足しただけで実行できます。
仕組み:type stripping は型を消すだけ
Node の type stripping は、.ts から型注釈(: string や interface など)を取り除いて実行します。型チェックはしません。 型が矛盾していてもそのまま走るため、node app.ts が通ることと「型が正しいこと」は別物です。型の検査は従来どおり tsc --noEmit を CI などで別に回してください。
もう一点、ES Module として実行する場合、相対インポートには拡張子が必要です。他のファイルを読み込むときは import { x } from './util.ts' のように .ts まで書きます。
恒久的にどうするか
- Node を 22.18 以上 / 24 に上げると、フラグなしで
node app.tsが通ります。チームや CI の Node もそろえると、環境ごとの差がなくなります。 - フラグを毎回書きたくない場合は、
NODE_OPTIONS=--experimental-strip-typesを環境変数に置く方法もあります。ただしこれは Node 22.6〜22.17 の一時的な対処で、版を上げれば不要になります。 - ビルドやウォッチ、複数ファイルの解決まで必要なら、
tsxなどの実行ツールやtscでのコンパイルを使う選択肢もあります。Node 単体の type stripping は「型を消して走らせる」だけなので、用途が広がったら実行ツール側に寄せてください。
切り分け(うまくいかないとき)
--experimental-strip-typesを付けても別のエラーになる:enum・runtime code を含む名前空間・parameter properties など、型を消すだけでは実行できない(=erasable でない)TypeScript 機能を使っているとERR_UNSUPPORTED_TYPESCRIPT_SYNTAXになる(--experimental-transform-typesが要る、またはtsxなどに寄せる)。型だけの注釈・型だけの名前空間はそのまま動く。まずは型注釈だけの最小ファイルで通るか確かめる。ERR_UNKNOWN_FILE_EXTENSIONではなく構文エラーが出る:package.jsonに"type": "module"が有る構成がERR_UNKNOWN_FILE_EXTENSION(ESM ローダが.tsを扱えない・当記事の検証経路)。"type": "module"が無いと Node は.tsを CommonJS の JavaScript として解析し、型注釈でつまずいてSyntaxErrorになる。どちらも原因は同じ(.tsを扱えない)で、対応も同じ(フラグを付けるか Node を上げる)。- CI でだけ出る/手元では通る:手元の Node が 22.18 以上や 24 で、CI が 22.6〜22.17 のことがある。両方の
node -vをそろえる。