Skip to content

Fix: TypeScript Decorators Not Working (experimentalDecorators)

FixDevs ·

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.

Most frameworks that use decorators today require legacy decorators with experimentalDecorators: true and emitDecoratorMetadata: true. The errors occur when:

  • 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.

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?

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"
  }
}

For related TypeScript issues, see Fix: TypeScript isolatedModules Error and Fix: TypeScript Property Does Not Exist on Type.

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