node app.ts で TypeScript をそのまま実行したとき、@Injectable() のような decorator を含んでいると、その行で止まります。
/app/app.ts:7
@Injectable()
^
SyntaxError: Invalid or unexpected token
SyntaxError: Invalid or unexpected token は decorator 専用のメッセージではなく、構文エラー全般で出ます。まず ^ が指している行を見てください。 そこが @ で始まる decorator(@Injectable() / @Component / @Entity など)なら、原因はこれです。
Node は decorator を「消す」ことも「変換」することもしない
Node が .ts を直接実行できるのは、型注釈を削り落として JavaScript として動かしているから(strip-only mode)です。この仕組みそのものは ERR_UNKNOWN_FILE_EXTENSION: “.ts” と ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX にあります。ここで必要なのは 1 点だけです。
decorator は型ではありません。 decorator は正しい TypeScript ですが、クラスやメソッドに実行時の振る舞いを付ける構文で、型注釈のように消すと意味が変わります。ストリッパは型しか消さないので decorator はそのまま残り、残った @ を JavaScript パーサが受け取って止まります。
Node.js 公式ドキュメント(Modules: TypeScript)は、decorator が TC39 の Stage 3 提案であることを理由に、変換されず parser error になる(not transformed and will result in a parser error)と明記しています。enum のように「これは変換が要る構文だ」と名指しされて ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX で止まるのではなく、decorator は素の JavaScript として解析され、汎用の SyntaxError になります。
エラー文(SyntaxError: Invalid or unexpected token)と ^ の位置(@ の行)は、モジュール形式によりません。スタックトレースだけは形式で変わります——CommonJS では wrapSafe / cjs/loader、ESM("type": "module" や .mts)では compileSourceTextModule / esm/loader を指します(どちらも実測)。スタックの中身で原因を見分けようとせず、^ が指す @ の行で判断してください。
enum の直し方(変換フラグ)は decorator を変換しません
enum・値を含む namespace・パラメータプロパティは、変換モードを有効にすれば動きます。
node --experimental-transform-types app.ts
decorator は、このフラグを付けても変わりません。変換フラグは decorator を変換せず、SyntaxError のまま止まります(実測)。しかもこのフラグは Node 26 で削除されました(node: bad option: --experimental-transform-types)。「実験的フラグを付ければ動くはず」は decorator には当てはまりません。
| 構文 | --experimental-transform-types(22.7.0〜24.x) |
|---|---|
enum / 値を含む namespace / パラメータプロパティ / import x = require() | 変換されて動く |
| decorator(legacy も TC39 も) | 変わらない(SyntaxError のまま) |
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX の直し方 が挙げる対処は enum 系には効きますが、decorator はその表に出てきません。
型チェックでも事前に捕まえられません
enum 系には、node で実行する前に型検査で捕まえる手(erasableSyntaxOnly)があります。decorator にはこれも効きません。 erasableSyntaxOnly: true を付けて tsc --noEmit に通しても、legacy・TC39 のどちらの decorator もエラーになりません(終了コード 0・実測)。TypeScript の erasableSyntaxOnly が弾く構文の一覧に decorator は含まれていない(標準 decorator は TypeScript 固有ではなく JavaScript の提案構文である)ためで、decorator は事前検知の網からも漏れます。CI の型検査は緑のまま、node app.ts の実行だけが落ちます。
直し方:decorator を変換できるツールチェーンで実行する
decorator を書き換える必要はありません。decorator を JavaScript へ変換する段を持つツールに載せます。
ビルドしてから実行する(tsc)。 恒久的な修正はこれです。tsconfig は decorator の形式で分かれます。
旧形式(@Injectable() のように (target) を受け取る legacy decorator。NestJS / Angular / TypeORM が使う)は experimentalDecorators を有効にします。
// tsconfig.json(旧形式)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"outDir": "dist"
}
}
TC39 の新形式((value, context) を受け取る、TypeScript 5.0 以降の標準 decorator)は、experimentalDecorators を付けません。付けると TypeScript が旧形式として型検査し、引数の数が合わずに error TS1238 で落ちます(実測)。target は変換の条件ではなく、出力先に合わせて選べます(ES2020 でも変換されます・実測)。
// tsconfig.json(TC39 の新形式)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist"
}
}
npx tsc --project tsconfig.json
node dist/app.js
実行するのは .ts ではなく、変換後の .js です。
開発中はそのまま実行できるランナーを使う。
npx tsx app.ts
tsx は内部で decorator を変換してから実行します(姉妹ケースで実測。[email protected] で legacy・TC39 とも動く)。同じ app.ts が、tsx では動き、node では SyntaxError になります。 開発を tsx で回していて、実行だけ node に替えたときに踏むのはこの差です。
ts-node / swc / esbuild も TypeScript を実行・変換できますが、decorator の扱いはツールごとに設定が要ります。swc は decorator の解析が既定で無効(jsc.parser.decorators)、esbuild は decorator 構文は変換するものの emitDecoratorMetadata を非対応と公言しています(NestJS の DI はこのメタデータに依存します)。これらは本記事では実測していません。@Injectable() のような DI つきの decorator なら、TypeScript の設定をそのまま解釈する tsc か ts-node が確実です。
切り分け
^が@の行を指しているか —SyntaxError: Invalid or unexpected tokenは構文エラー全般で出る汎用メッセージです。decorator が原因なら、エラーはその@...の行を指します。別の行を指しているなら、原因は decorator ではありません——その行のコード(全角スペース、閉じ忘れ、壊れた文字列リテラルなど、通常の構文エラー)を見直してください。.tsをそもそも実行できていない(ERR_UNKNOWN_FILE_EXTENSION)なら 別の記事 です。--experimental-transform-typesを付けても同じ — decorator はこのフラグでは変換されません(Node 26 ではフラグ自体がありません)。enumなら動くので、ここで decorator とenum系は分かれます。tsc --noEmitは通る/erasableSyntaxOnlyでも通る — decorator は正しい TypeScript なので型検査は通ります。erasableSyntaxOnlyを付けても decorator は弾かれません(実測)。止まるのはnode app.tsの実行時だけです。- エラーが
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(末尾がenum/namespace declaration/parameter propertyなど) → それは変換モードで直る側です。ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX の直し方 を参照してください。decorator はそちらの表には出てきません。 tsx/ts-nodeでは動いていた → それらは decorator を変換します。Node の組み込みサポートは変換しないので、同じコードでも動く/動かないが分かれます。
検証環境
node:24.18.0-alpine- 再現:旧形式の decorator(
@Injectable())を含む.tsをnode reproduce/app.tsで実行し、SyntaxError: Invalid or unexpected tokenが@Injectable()の行を指して終了コード 1 - 修正:同一のソースを
tsc(experimentalDecorators: true・[email protected])でコンパイルし、node dist/app.jsを実行して終了コード 0・シグネチャ消滅。decorator は書き換えていません。出力(decorated=Worker/started)も再現側と同じ - 同じ
SyntaxError(@の行を指す)になるのを確認した範囲:node --experimental-transform-types reproduce/app.ts(終了コード 1・ERR_UNSUPPORTED_TYPESCRIPT_SYNTAXにはならない)/TC39 形式の decorator(strip・transform とも)/ESM("type": "module"と.mts)でも同じメッセージ・同じ^位置(ただしスタックはesm/loaderを指す)/node:26(26.5.0)でも同じSyntaxError node:26ではnode --experimental-transform-typesがbad option(終了コード 9)=フラグ自体が存在しません(削除は v26.0.0)- TC39 形式(
experimentalDecoratorsなし)のtscビルド:target: ES2020/ES2022のどちらでも変換・実行できました(終了コード 0)。TC39 のソースにexperimentalDecorators: trueを当てるとerror TS1238(デコレータの引数不一致)で落ちます。いずれも[email protected] erasableSyntaxOnly: trueを付けたtsc --noEmit:legacy・TC39 の decorator はどちらも弾かれません(終了コード 0)tsxが decorator を変換して実行することは、姉妹ケース(unsupported-typescript-syntax-enum)の probe で観測しています([email protected])。ts-node/swc/esbuildは本記事では実測していません