Skip to content

Fix: TypeScript Decorators Not Working (experimentalDecorators)

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix TypeScript decorators not applying — experimentalDecorators not enabled, emitDecoratorMetadata missing, reflect-metadata not imported, and decorator ordering issues.

The Error

You use a decorator in TypeScript and get a compile error:

error TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig.json' or 'jsconfig.json' to remove this warning.

Or in TypeScript 5+:

error TS1241: Unable to resolve signature of class decorator when called as an expression.
  Type 'typeof Component' is not assignable to type 'new (...args: any[]) => any'.

Or the decorator compiles but doesn’t apply at runtime — the class behaves as if the decorator was never there:

@Injectable()
class UserService {
  getUser() { return 'Alice'; }
}

// At runtime: Reflect.getMetadata is undefined
// TypeError: Reflect.getMetadata is not a function

Or dependency injection frameworks (NestJS, TypeORM, InversifyJS) fail with metadata errors:

Error: Nest can't resolve dependencies of the UserService.
Please make sure that the argument at index [0] is available in the AppModule context.

Why This Happens

TypeScript has two separate decorator systems that behave differently:

  1. Legacy decorators (experimentalDecorators: true) — the Stage 1 proposal implementation, used by NestJS, Angular, TypeORM, and most current frameworks. Requires experimentalDecorators in tsconfig.json.
  2. TC39 decorators (TypeScript 5.0+, no flag needed) — the finalized Stage 3 proposal. Incompatible with legacy decorators and with reflect-metadata.

These two systems have different runtime semantics, different signatures, and different relationships with reflect-metadata. A function written for one system will not work as a decorator in the other. TypeScript 5.0 made TC39 decorators the default for new projects but kept the legacy mode behind experimentalDecorators: true so existing frameworks would not break. The confusion arises because both look identical at the call site — @MyDecorator class Foo {} is valid syntax in both modes. The difference is what the compiler emits, and that determines whether your framework finds the metadata it needs.

The most common failure path is that tsconfig.json is missing experimentalDecorators or emitDecoratorMetadata, the build emits TC39-shaped decorator code, and your framework — built against the legacy shape — silently does nothing. The class compiles, the decorator runs, but the wiring (dependency injection, route registration, column inference) never happens. Concrete root causes:

  • experimentalDecorators is not set to true in tsconfig.json.
  • emitDecoratorMetadata is missing — frameworks that use Reflect.metadata (NestJS, TypeORM, InversifyJS) require this flag to emit type metadata.
  • reflect-metadata is not importedReflect.metadata doesn’t exist in JavaScript without this polyfill. It must be imported once, before any decorated class is loaded.
  • Using legacy decorators with TypeScript 5 TC39 decorators — TypeScript 5+ introduced TC39 decorators by default. If you try to mix the two systems, compilation fails.
  • Wrong decorator order — decorators apply bottom-up on a class. Placing them in the wrong order causes unexpected behavior.
  • Bundler stripping decorators — esbuild, SWC, and Vite each have separate decorator handling that does not pick up tsconfig automatically.

Version History That Changes the Failure Mode

The TC39 decorators proposal advanced to Stage 3 in March 2022, formally locking the shape of the new API after almost a decade of revisions through Stages 0, 1, and 2. TypeScript shipped that Stage 3 implementation in TypeScript 5.0 (March 16, 2023) under the decorators switch. From 5.0 onward, writing @MyDecorator in a .ts file without experimentalDecorators: true no longer triggers an error — it compiles as a TC39 decorator. This single change is the source of most “my decorator stopped working after upgrading TypeScript” reports.

Key version landmarks:

  • TypeScript 1.5 (Jul 2015) introduced the original experimental decorator implementation tied to the Stage 1 proposal. This is the dialect Angular and NestJS were built on.
  • TypeScript 2.1 (Dec 2016) added emitDecoratorMetadata, which made framework-style dependency injection possible by emitting Reflect.metadata("design:type", ...) calls.
  • TypeScript 4.9 (Nov 2022) stabilized useDefineForClassFields. With ES2022 target, this flag became true by default — and that breaks legacy decorators that mutate class fields.
  • TypeScript 5.0 (Mar 16, 2023) shipped TC39 Stage 3 decorators. New behavior: ClassDecoratorContext, ClassMethodDecoratorContext, ClassFieldDecoratorContext. No reflect-metadata integration.
  • TypeScript 5.2 (Aug 2023) improved error messages when mixing the two decorator systems and added better support for using declarations alongside decorators.
  • TypeScript 5.3 (Nov 2023) added narrower types in the new decorator context objects.
  • TypeScript 5.4–5.5 (2024) continued to refine the Stage 3 implementation and stabilized class field initialization order with decorators applied.

The class field initialization order is worth calling out. In legacy decorator mode, a decorator on a class field could run before the field’s initializer assigned its value. In TC39 mode, the decorator receives an explicit addInitializer hook and runs in a defined order relative to the field’s value. Code written for legacy decorators that assumes field-level decorators can mutate field values directly will silently fail under TC39 decorators.

For framework users, the practical effect: NestJS, TypeORM, InversifyJS, and class-validator all still require legacy decorators as of 2026. They have not migrated to TC39 yet, and the migration is non-trivial because reflect-metadata has no equivalent in the TC39 design. If you are on any of those frameworks, keep experimentalDecorators: true and emitDecoratorMetadata: true set, even on TypeScript 5.4 or newer. Angular has its own decorator transformer (via the Angular compiler) and is decoupled from this distinction.

Fix 1: Enable experimentalDecorators in tsconfig.json

Add the required flags to your tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "experimentalDecorators": true,     // Required for legacy decorators
    "emitDecoratorMetadata": true,       // Required for NestJS, TypeORM, InversifyJS
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Note: emitDecoratorMetadata requires TypeScript to emit Reflect.metadata() calls for every decorated class. Without reflect-metadata imported, these calls throw at runtime even if compilation succeeds.

Verify the tsconfig is being used:

# Show the resolved tsconfig
npx tsc --showConfig

# Or specify the config explicitly
npx tsc --project tsconfig.json --showConfig

Check for multiple tsconfig files — projects often have tsconfig.json, tsconfig.build.json, and tsconfig.test.json. Make sure the right one has experimentalDecorators: true:

find . -name "tsconfig*.json" -not -path "*/node_modules/*"
# ./tsconfig.json
# ./tsconfig.build.json
# ./apps/api/tsconfig.json   ← This may have its own settings

Fix 2: Import reflect-metadata

Frameworks that use Reflect.metadata require you to import the reflect-metadata polyfill once, at the application entry point, before any decorated code runs:

npm install reflect-metadata
// main.ts or index.ts — the very first import
import 'reflect-metadata';

// All other imports come after
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Common Mistake: Importing reflect-metadata inside a module file instead of at the application entry point. By the time the decorator runs, Reflect.metadata may not be initialized yet for other modules that load before the import executes.

Verify reflect-metadata is working:

import 'reflect-metadata';

function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log('Metadata:', Reflect.getMetadata('design:type', target, key));
  return descriptor;
}

class Example {
  @Log
  value: string = '';
}
// If Reflect.getMetadata works, the decorator is applying correctly

Fix 3: Fix TypeScript 5 Decorator Compatibility

TypeScript 5 introduced TC39 Stage 3 decorators as the default. If you’re using TypeScript 5 with legacy decorators (NestJS, TypeORM, etc.), you need experimentalDecorators: true to opt back into legacy mode:

// tsconfig.json — for NestJS / TypeORM with TypeScript 5
{
  "compilerOptions": {
    "target": "ES2021",
    "experimentalDecorators": true,    // Opt into legacy decorator mode
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": false   // Required with experimentalDecorators in TS 5
  }
}

Important: In TypeScript 5 with "target": "ES2022" or higher, useDefineForClassFields defaults to true, which breaks legacy decorators. Set it to false explicitly when using experimentalDecorators.

Check your TypeScript version:

npx tsc --version
# Version 5.x.x → needs useDefineForClassFields: false with experimentalDecorators

If you’re starting a new project with TS5+ and don’t need framework decorators:

TC39 decorators (no experimentalDecorators) work differently and don’t support emitDecoratorMetadata. Use them for standalone decorator use cases:

// TC39 decorators (TypeScript 5+, no tsconfig flag needed)
function sealed(target: typeof SomeClass, ctx: ClassDecoratorContext) {
  Object.seal(target);
  Object.seal(target.prototype);
}

@sealed
class SomeClass {
  name = 'example';
}

Fix 4: Fix NestJS Decorator Errors

NestJS relies heavily on decorators and reflect-metadata. The most common NestJS-specific issues:

Missing reflect-metadata import:

// main.ts
import 'reflect-metadata';  // Must be first
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

NestJS tsconfig.json requirements:

{
  "compilerOptions": {
    "module": "CommonJS",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "ES2021",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
  }
}

Common NestJS injection error caused by missing metadata:

// Wrong — circular import causes metadata to be lost
import { UserModule } from './user/user.module';  // Circular dependency
// Fix — use forwardRef for circular dependencies
import { forwardRef, Module } from '@nestjs/common';

@Module({
  imports: [forwardRef(() => UserModule)],
})
export class AuthModule {}

Fix 5: Fix TypeORM Decorator Errors

TypeORM entities use decorators for column definitions. Missing emitDecoratorMetadata prevents TypeORM from inferring column types:

// Wrong — without emitDecoratorMetadata, TypeORM can't infer the column type
@Entity()
class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()              // Type inference fails without emitDecoratorMetadata
  name: string;
}

TypeORM tsconfig.json requirements:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictPropertyInitialization": false  // TypeORM initializes columns at runtime
  }
}

Explicitly specify column types to avoid relying on metadata:

@Entity()
class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 255 })  // Explicit type — doesn't need metadata
  name: string;

  @Column({ type: 'int' })
  age: number;
}

Fix 6: Fix Decorator Execution Order

Decorators on a class are evaluated top-to-bottom but applied bottom-to-top. Method decorators execute in reverse order:

function First() {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    console.log('First applied');
    return descriptor;
  };
}

function Second() {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    console.log('Second applied');
    return descriptor;
  };
}

class Example {
  @First()
  @Second()
  method() {}
}

// Output:
// Second applied  ← bottom decorator applies first
// First applied   ← top decorator applies second (wraps around Second)

Real-world scenario: In NestJS, placing @UseGuards() after @Get() causes the guard to run but the route metadata may not yet be attached. Place authentication/authorization decorators after route decorators:

// Correct order — route definition first, then guards/interceptors
@Get(':id')
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor)
async getUser(@Param('id') id: string) {
  return this.userService.findOne(id);
}

Still Not Working?

Check whether ts-node is using its own tsconfig. ts-node reads tsconfig.json by default but ignores tsconfig.build.json and similar. If you run your app with ts-node and decorators work in compiled output but not in dev, add a ts-node block to your tsconfig:

{
  "ts-node": {
    "compilerOptions": {
      "experimentalDecorators": true,
      "emitDecoratorMetadata": true,
      "module": "CommonJS"
    }
  }
}

Check the import order for reflect-metadata. Even with the polyfill installed, a single import in a downstream module is not enough. Some bundlers reorder imports during tree-shaking. The safest pattern is to import reflect-metadata as the very first line of your entry file and verify it appears in the bundled output:

# After build, grep the entry chunk
grep -m1 reflect-metadata dist/main.js
# Should appear near the top of the file

Verify the compiled output includes decorator calls:

npx tsc --noEmit false --outDir ./dist-debug
cat ./dist-debug/src/user.service.js | grep -A5 "__decorate"
# Should show: __decorate([Injectable()], UserService)
# If not present, experimentalDecorators is not active

Check if a bundler is stripping decorators — Vite, esbuild, and SWC have different decorator handling than the TypeScript compiler. Configure them explicitly:

// vite.config.ts — use babel for decorator transform
import { defineConfig } from 'vite';

export default defineConfig({
  esbuild: {
    target: 'es2021',
  },
  plugins: [
    // For legacy decorators with Vite:
    // npm install @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties
  ]
});

For esbuild/tsup — enable decorator support:

// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs'],
  experimentalDts: true,
  esbuildOptions(options) {
    options.keepNames = true;  // Required for decorator metadata
  },
});

For SWC (used by NestJS CLI in recent versions):

// .swcrc or nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "compilerOptions": {
    "builder": {
      "type": "swc",
      "options": {
        "swcrcPath": ".swcrc"
      }
    }
  }
}
// .swcrc
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "target": "es2021"
  },
  "module": {
    "type": "commonjs"
  }
}

Check the TypeScript version with which the framework was built. NestJS 10 ships against TypeScript 5.1; if you install TypeScript 5.4 in your project, certain narrowing changes in the compiler can produce type errors in decorator definitions without any change to runtime behavior. Pin the TypeScript version your framework recommends:

npm ls typescript
# Verify only one TypeScript version is installed

Use a clean tsconfig probe. Copy a single decorated class into a fresh project with only experimentalDecorators and emitDecoratorMetadata set, install reflect-metadata, and run it. If decorators work there but not in your project, the cause is your build pipeline (Vite, esbuild, Turbo, Nx), not the decorator code.

For related TypeScript issues, see Fix: TypeScript isolatedModules Error, Fix: TypeScript Property Does Not Exist on Type, Fix: TypeScript Cannot Find Module, and Fix: NestJS Circular Dependency.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles