Fix: TypeScript Decorators Not Working (experimentalDecorators)
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 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.
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:
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.
- 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 emittingReflect.metadata("design:type", ...)calls. - TypeScript 4.9 (Nov 2022) stabilized
useDefineForClassFields. With ES2022 target, this flag becametrueby 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. Noreflect-metadataintegration. - TypeScript 5.2 (Aug 2023) improved error messages when mixing the two decorator systems and added better support for
usingdeclarations 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:
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?
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 fileVerify 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"
}
}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 installedUse 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.
Fix: Sharp Not Working — Installation Failing, Image Not Processing, or Build Errors on Deploy
How to fix Sharp image processing issues — native binary installation, resize and convert operations, Next.js image optimization, Docker setup, serverless deployment, and common platform errors.