Error [SyntaxError: Invalid or unexpected token]

SyntaxError: Invalid or unexpected token の直し方(Node の型ストリッピングは decorator を実行できない)

FIX SUMMARY verified
Applies when
node:24.18.0-alpineNode's strip-only TypeScript cannot erase or transform decorators; @decorator surfaces as a generic SyntaxError, and --experimental-transform-types does not help (removed in Node 26)

Verified: reproduced in node:24.18.0-alpine, then the SyntaxError: Invalid or unexpected token signature was gone after the fix (exit 0).

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 の設定をそのまま解釈する tscts-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())を含む .tsnode reproduce/app.ts で実行し、SyntaxError: Invalid or unexpected token@Injectable() の行を指して終了コード 1
  • 修正:同一のソースtscexperimentalDecorators: 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-typesbad 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 は本記事では実測していません

検証(machine-verified)

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

verify — run-case.mjs
$ node run-case.mjs node/typescript-decorator-strip-syntaxerror
● reproduce SyntaxError: Invalid or unexpected token present ✓
● apply fix exit 0
● re-run SyntaxError: Invalid or unexpected token gone ✓
PASS verified · node:24.18.0-alpine · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。