Error [ERR_UNKNOWN_FILE_EXTENSION]

Unknown file extension ".ts" の直し方(Node.js で TypeScript を直接実行したとき)

FIX SUMMARY verified
Applies when
node:22.6.0-alpinenode app.ts (no loader)

Verified: reproduced in node:22.6.0-alpine, then the ERR_UNKNOWN_FILE_EXTENSION signature was gone after the fix (exit 0).

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 declarationUnexpected 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 以上 / 24erasable な構文(型注釈のみ等)はそのまま実行できる型注釈だけなら対応不要
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 から型注釈(: stringinterface など)を取り除いて実行します。型チェックはしません。 型が矛盾していてもそのまま走るため、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 をそろえる。

検証(machine-verified)

この修正は node:22.6.0-alpine のバージョン固定コンテナ内で再現し、修正後に ERR_UNKNOWN_FILE_EXTENSION のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/err-unknown-file-extension-ts
● reproduce ERR_UNKNOWN_FILE_EXTENSION present ✓
● apply fix exit 0
● re-run ERR_UNKNOWN_FILE_EXTENSION gone ✓
PASS verified · node:22.6.0-alpine · signature gone