Skip to content
C Codeloom
TypeScript

TypeScript Decorators: The 2026 Standard Decorators

Understand TypeScript's modern standard decorators: how they differ from the legacy proposal, the context API, and practical class and method examples.

·6 min read · By Yash Kesharwani
Intermediate 10 min read

What you'll learn

  • What changed between legacy and standard decorators
  • How the decorator context object works
  • Writing class, method, and accessor decorators
  • Using addInitializer for per-instance setup
  • Where decorators fit in modern TypeScript projects

Prerequisites

  • TypeScript basics from /blog/typescript-basic-types
  • Generics from /blog/typescript-generics-basics

Decorators in TypeScript have a long, complicated history. For years, the language shipped an experimental decorator implementation behind a compiler flag, while the TC39 proposal kept evolving. As of TypeScript 5.0 and onward, the language ships standard decorators based on the Stage 3 proposal, which is what current and future runtimes adopt. This article focuses on those standard decorators, which are what you should use in new code in 2026.

What a Decorator Is

A decorator is a function applied to a class, method, accessor, field, or auto-accessor at definition time. It receives the thing it decorates plus a context object, and it can return a replacement or perform side effects. Decorators run when the class is defined, not when instances are created.

function logged(value: Function, context: ClassMethodDecoratorContext) {
  const name = String(context.name);
  return function (this: unknown, ...args: unknown[]) {
    console.log(`calling ${name}`);
    const result = value.apply(this, args);
    console.log(`returned from ${name}`);
    return result;
  };
}

class Greeter {
  @logged
  greet(name: string) {
    return `Hello, ${name}`;
  }
}

When you call new Greeter().greet(‘Ada’), the logged wrapper runs around the original method.

The Context Object

Every decorator receives a context object whose shape depends on what is being decorated. Common fields include kind, which describes the target, name, which identifies it, and access, which provides typed getters and setters. Class decorators get a different context than method or field decorators.

type Kinds =
  | 'class'
  | 'method'
  | 'getter'
  | 'setter'
  | 'field'
  | 'accessor';

The context is the cleanest part of the new design. Legacy decorators received raw descriptors and required manual juggling of writability and enumerability. The new context API gives you typed helpers like addInitializer for per-instance setup and access for read and write of private fields.

Class Decorators

A class decorator receives the class constructor and the context. It can replace the class with a new one or attach metadata.

function registered<C extends new (...args: any[]) => any>(
  value: C,
  context: ClassDecoratorContext,
) {
  console.log(`registering ${String(context.name)}`);
  return class extends value {
    registeredAt = new Date();
  };
}

@registered
class Widget {
  constructor(public id: string) {}
}

The returned class extends the original, which is the recommended pattern. Returning a fresh constructor that does not extend the original breaks inheritance, so prefer subclassing.

Method Decorators

Method decorators are the most common. They receive the method function and a context. Returning a new function replaces the method on the prototype.

function throttle(ms: number) {
  return function (value: Function, context: ClassMethodDecoratorContext) {
    let last = 0;
    return function (this: unknown, ...args: unknown[]) {
      const now = Date.now();
      if (now - last < ms) return;
      last = now;
      return value.apply(this, args);
    };
  };
}

class Tracker {
  @throttle(1000)
  ping() {
    console.log('ping');
  }
}

Note that the closure variable last is shared across all instances because the decorator runs once when the class is defined. For per-instance state, use addInitializer.

Per-Instance State With addInitializer

The context object’s addInitializer method registers a callback that runs once per instance, right after construction. This is the modern way to attach per-instance data.

function autobind(value: Function, context: ClassMethodDecoratorContext) {
  context.addInitializer(function (this: any) {
    this[context.name] = value.bind(this);
  });
}

class Counter {
  count = 0;

  @autobind
  increment() {
    this.count += 1;
  }
}

const c = new Counter();
const inc = c.increment;
inc();
console.log(c.count);

The autobind decorator gives you a method that keeps its this binding even when detached, similar to arrow methods but without runtime overhead per call.

Accessor and Field Decorators

Accessor decorators wrap a getter or setter. Auto-accessor decorators, declared with the accessor keyword, allow you to intercept both read and write paths cleanly.

function clamp(min: number, max: number) {
  return function (target: ClassAccessorDecoratorTarget<unknown, number>, _ctx: ClassAccessorDecoratorContext) {
    return {
      get() { return target.get.call(this); },
      set(value: number) {
        const v = Math.max(min, Math.min(max, value));
        target.set.call(this, v);
      },
      init(value: number) { return Math.max(min, Math.min(max, value)); },
    };
  };
}

class Volume {
  @clamp(0, 100)
  accessor level = 50;
}

Field decorators are similar but operate on initial values rather than getters and setters. They receive an initializer function and can return a new one.

Stacking and Order

Multiple decorators on the same target run from the bottom up. The decorator closest to the declaration runs first, and its result is then passed to the next one upward. Initializer callbacks added via addInitializer run in a predictable order, but if you depend on cross-decorator ordering, document it carefully.

class Pipeline {
  @logged
  @throttle(500)
  send(payload: unknown) { /* ... */ }
}

Here throttle wraps send first, and then logged wraps the throttled version.

Differences From Legacy Decorators

Standard decorators do not see or set property descriptors directly. They cannot decorate parameters, which was a legacy feature used heavily by dependency injection frameworks. They run before the class body finishes executing for fields, which means decorators cannot inspect sibling values at decoration time.

If you maintain a codebase that relies on legacy decorator metadata via experimentalDecorators and emitDecoratorMetadata, you can keep using it during migration, but new code should target the standard form. Frameworks like NestJS and TypeORM have begun shipping standard-decorator-compatible APIs.

When to Use Decorators

Decorators are great for cross-cutting concerns that operate at the class shape level: logging, caching, validation, dependency wiring, and ORM mapping. They are less appropriate for things that depend on runtime values or that need to be visible inline at call sites.

For one-off transformations, a higher-order function is often clearer. For systemic patterns repeated across dozens of classes, a decorator is more elegant.

For deeper TypeScript foundations, see /blog/typescript-basic-types, /blog/typescript-generics-basics, and /blog/typescript-utility-types.

Wrap up

Standard decorators in TypeScript 5 and beyond are a clean, well-typed way to annotate classes, methods, and accessors. The context API removes the descriptor headaches of the legacy proposal, and addInitializer gives you a principled hook for per-instance state. Use them for cross-cutting concerns, and you will write less glue code in modern TypeScript projects.