Fix: TypeScript Decorators Not Working (experimentalDecorators)
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 functionOr 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:
- Legacy decorators (
experimentalDecorators: true) — the Stage 1 proposal implementation, used by NestJS, Angular, TypeORM, and most current frameworks. RequiresexperimentalDecoratorsintsconfig.json. - 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:
experimentalDecoratorsis not set totrueintsconfig.json.emitDecoratorMetadatais missing — frameworks that useReflect.metadata(NestJS, TypeORM, InversifyJS) require this flag to emit type metadata.reflect-metadatais not imported —Reflect.metadatadoesn’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:
emitDecoratorMetadatarequires TypeScript to emitReflect.metadata()calls for every decorated class. Withoutreflect-metadataimported, 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 --showConfigCheck 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 settingsFix 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-metadatainside a module file instead of at the application entry point. By the time the decorator runs,Reflect.metadatamay 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 correctlyFix 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,useDefineForClassFieldsdefaults totrue, which breaks legacy decorators. Set it tofalseexplicitly when usingexperimentalDecorators.
Check your TypeScript version:
npx tsc --version
# Version 5.x.x → needs useDefineForClassFields: false with experimentalDecoratorsIf 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 activeCheck 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Express req.body Is undefined
How to fix req.body being undefined in Express — missing body-parser middleware, wrong Content-Type header, middleware order issues, and multipart form data handling.
Fix: Node.js Crashing with UnhandledPromiseRejection (--unhandled-rejections)
How to fix Node.js UnhandledPromiseRejectionWarning and process crashes — why unhandled promise rejections crash Node.js 15+, how to add global handlers, find the source of the rejection, and fix async error handling.
Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)
How to fix Next.js middleware not executing — wrong file location, matcher config errors, middleware not intercepting API routes, and how to debug middleware execution in Next.js 13 and 14.
Fix: TypeScript isolatedModules Errors (const enum, type-only imports)
How to fix TypeScript isolatedModules errors — why const enum fails with Babel and Vite, how to replace const enum, fix re-exported types, and configure isolatedModules correctly for your build tool.