TypeScript 5 Decorators: What Changed and Why It Matters


If you’ve been using TypeScript decorators with frameworks like NestJS or Angular, you’ve been using a syntax that was never formally standardised. The experimentalDecorators flag in tsconfig.json enabled a proposal that dates back to 2015 and diverged significantly from what TC39 eventually agreed upon.

TypeScript 5 introduced support for the official TC39 Stage 3 decorator proposal, and the differences are more than cosmetic. Here’s what you need to know.

The Old Way vs The New Way

Legacy decorators (the ones behind experimentalDecorators) receive different arguments depending on what they’re decorating. A class decorator gets the constructor. A method decorator gets the target, property key, and property descriptor. The API is inconsistent and relies heavily on reflect-metadata for type information.

The new standard decorators use a unified API. Every decorator receives two arguments: the value being decorated and a context object. The context object contains metadata about what’s being decorated — its name, whether it’s static, whether it’s private, and an addInitializer function for running setup code.

function logged(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);

  function replacementMethod(this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with`, args);
    const result = originalMethod.call(this, ...args);
    console.log(`${methodName} returned`, result);
    return result;
  }

  return replacementMethod;
}

class Calculator {
  @logged
  add(a: number, b: number): number {
    return a + b;
  }
}

This is cleaner. The decorator receives the method itself and a context object. No more juggling property descriptors.

What Can You Decorate?

Standard decorators support classes, methods, getters, setters, fields, and accessors. The new accessor keyword is particularly interesting — it generates a getter/setter pair from a field declaration, giving decorators a clean way to intercept property access.

function observable(target: undefined, context: ClassAccessorDecoratorContext) {
  return {
    get(this: any) {
      return context.access.get(this);
    },
    set(this: any, value: any) {
      console.log(`Setting ${String(context.name)} to ${value}`);
      context.access.set(this, value);
    },
    init(initialValue: any) {
      return initialValue;
    }
  };
}

class UserProfile {
  @observable
  accessor name: string = "";
}

The Migration Challenge

Here’s the uncomfortable reality: you can’t mix old and new decorator syntax in the same project. If you have experimentalDecorators: true in your tsconfig, all decorators use the legacy behaviour. Remove it, and they all use the standard behaviour.

For applications built on NestJS or Angular, this means you’re stuck with legacy decorators until those frameworks fully support the new standard. NestJS has been working on compatibility, but it’s not a simple switch — the entire dependency injection system relies on reflect-metadata, which doesn’t exist in the new proposal.

Metadata Without reflect-metadata

The new decorator proposal includes a metadata mechanism, but it works differently. Instead of reflection, decorators can attach metadata to classes through the context object’s metadata property. This metadata is then accessible via Symbol.metadata on the class.

const VALIDATION_KEY = Symbol("validation");

function minLength(min: number) {
  return function (target: undefined, context: ClassFieldDecoratorContext) {
    context.metadata[VALIDATION_KEY] ??= {};
    context.metadata[VALIDATION_KEY][context.name] = { min };
  };
}

This is more explicit and doesn’t require a separate polyfill library, but it’s also less powerful than what reflect-metadata provided. You lose the ability to automatically infer parameter types, which is the backbone of how NestJS does dependency injection.

Should You Switch Now?

If you’re starting a new project that doesn’t depend on a framework requiring legacy decorators, use the standard syntax. It’s the future, and the API is genuinely better designed.

If you’re maintaining an existing NestJS or Angular application, stay with experimentalDecorators for now. The ecosystem needs more time to adapt, and forcing a migration will create more problems than it solves.

For library authors, consider supporting both. You can use conditional types and overloads to provide APIs that work with either decorator flavour, though this adds maintenance burden.

The Bigger Picture

The decorator standardisation is part of a broader trend in TypeScript: aligning more closely with JavaScript standards rather than pioneering its own syntax. We saw this with the deprecation of namespace in favour of ES modules, and with the push toward using satisfies instead of type assertions.

TypeScript’s strength has always been its ability to type-check existing JavaScript patterns rather than inventing new runtime behaviour. Standard decorators fit that philosophy better than the legacy proposal ever did.

Keep an eye on the TC39 decorator metadata proposal (Stage 3) for additional capabilities. Once that lands, the gap between legacy and standard decorators will narrow significantly, and migration will become more practical for framework-dependent projects.