BytePane

TypeScript Decorators: A Complete Guide for 2026

TypeScript15 min read

What Are Decorators?

Decorators are a way to add metadata or modify the behavior of classes, methods, properties, and parameters using a declarative syntax. They use the @expression syntax placed immediately before the declaration they modify. If you have used frameworks like Angular, NestJS, or TypeORM, you have already been using decorators.

TypeScript 5.0 introduced native support for the TC39 Stage 3 decorator proposal, which differs from the legacy experimental decorators that TypeScript has supported since version 1.5. In 2026, the TC39 standard is the recommended approach for all new projects. Legacy decorators remain available via the experimentalDecorators flag for backward compatibility.

At their core, decorators are functions. A decorator receives information about the thing it decorates and can optionally return a replacement. This simple concept enables powerful patterns like dependency injection, logging, validation, access control, and memoization.

Setting Up Decorators in Your Project

To use TC39 standard decorators, you need TypeScript 5.0 or later. No special compiler flags are required. For legacy decorators (used by Angular and NestJS as of early 2026), you need to enable the experimental flag.

// tsconfig.json for TC39 standard decorators (recommended)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "strict": true
    // No special flag needed — decorators work out of the box
  }
}

// tsconfig.json for legacy (experimental) decorators
{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // Required by some frameworks
  }
}

You can validate your tsconfig.json structure by pasting it into our JSON Formatter to catch syntax errors before they cause cryptic compiler failures.

Class Decorators

A class decorator is applied to the class constructor. It receives the class itself (the constructor function) and an optional context object. Class decorators can modify the class, replace it, or add metadata.

// TC39 Standard Class Decorator
function sealed(target: Function, context: ClassDecoratorContext) {
  Object.seal(target);
  Object.seal(target.prototype);
  console.log(`Class ${String(context.name)} has been sealed`);
}

@sealed
class UserService {
  getUser(id: number) {
    return { id, name: "Alice" };
  }
}

// Decorator with arguments (decorator factory)
function entity(tableName: string) {
  return function (target: Function, context: ClassDecoratorContext) {
    (target as any).tableName = tableName;
  };
}

@entity("users")
class User {
  id!: number;
  name!: string;
}

console.log((User as any).tableName); // "users"

The decorator factory pattern (a function that returns a decorator) is essential when you need to pass configuration to your decorator. This is how frameworks like NestJS implement @Controller('/users') and TypeORM implements @Entity('users').

Method Decorators

Method decorators are the most commonly used type. They wrap a method to add behavior before, after, or around its execution. In the TC39 standard, a method decorator receives the original method and a context object, and can return a replacement method.

// Logging decorator
function log(
  originalMethod: Function,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${String(context.name)} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${String(context.name)} returned:`, result);
    return result;
  };
}

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

const calc = new Calculator();
calc.add(2, 3);
// Logs: Calling add with args: [2, 3]
// Logs: add returned: 5

Memoization Decorator

A practical example is a memoization decorator that caches the results of expensive function calls.

function memoize(
  originalMethod: Function,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, any>();

  return function (this: any, ...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class DataService {
  @memoize
  fetchExpensiveData(query: string): object {
    // Simulated expensive operation
    console.log("Computing...");
    return { query, timestamp: Date.now() };
  }
}

const svc = new DataService();
svc.fetchExpensiveData("users"); // Logs "Computing..."
svc.fetchExpensiveData("users"); // Returns cached result, no log

Property Decorators

Property decorators let you intercept property access and assignment. In TC39 decorators, they work through accessor decorators using the accessor keyword, which creates auto-accessors (getter/setter pairs backed by a private field).

// Validation decorator for properties
function minLength(min: number) {
  return function (
    target: ClassAccessorDecoratorTarget<any, string>,
    context: ClassAccessorDecoratorContext
  ) {
    return {
      set(this: any, value: string) {
        if (value.length < min) {
          throw new Error(
            `${String(context.name)} must be at least ${min} characters`
          );
        }
        target.set.call(this, value);
      },
      get(this: any) {
        return target.get.call(this);
      },
    } as ClassAccessorDecoratorResult<any, string>;
  };
}

class UserProfile {
  @minLength(3)
  accessor username: string = "guest";

  @minLength(8)
  accessor password: string = "defaultpw";
}

const profile = new UserProfile();
profile.username = "Al"; // Error: username must be at least 3 characters
profile.username = "Alice"; // OK

Parameter Decorators

Parameter decorators annotate individual method parameters. They are widely used in dependency injection frameworks to mark parameters that should be automatically resolved from a container. Note that TC39 standard decorators do not include parameter decorators -- this feature remains exclusive to the legacy experimentalDecorators mode.

// Parameter decorator (legacy/experimental only)
function Inject(token: string) {
  return function (
    target: Object,
    propertyKey: string | symbol,
    parameterIndex: number
  ) {
    const existingInjections: Array<{ index: number; token: string }> =
      Reflect.getMetadata("injections", target, propertyKey) || [];

    existingInjections.push({ index: parameterIndex, token });
    Reflect.defineMetadata(
      "injections",
      existingInjections,
      target,
      propertyKey
    );
  };
}

class OrderController {
  constructor(
    @Inject("OrderService") private orderService: any,
    @Inject("Logger") private logger: any
  ) {}
}

If your project uses NestJS or Angular, you are already using parameter decorators via @Inject(). These frameworks currently require experimentalDecorators: true in your tsconfig.

Decorator Composition and Execution Order

Multiple decorators can be applied to a single declaration. They are evaluated top-to-bottom but executed bottom-to-top (like function composition). This order matters when decorators depend on each other.

function first(
  originalMethod: Function,
  context: ClassMethodDecoratorContext
) {
  console.log("first() evaluated");
  return function (this: any, ...args: any[]) {
    console.log("first() executed");
    return originalMethod.apply(this, args);
  };
}

function second(
  originalMethod: Function,
  context: ClassMethodDecoratorContext
) {
  console.log("second() evaluated");
  return function (this: any, ...args: any[]) {
    console.log("second() executed");
    return originalMethod.apply(this, args);
  };
}

class Example {
  @first
  @second
  method() {
    return "result";
  }
}

// Evaluation order: first() evaluated → second() evaluated
// Execution order:  first() executed → second() executed → method()
// (first wraps second, second wraps the original method)

Practical Patterns: Building a REST API Framework

Decorators shine in framework design. Here is a simplified example showing how NestJS-style route decorators work under the hood. This pattern combines class, method, and factory decorators to build a declarative routing system.

// Route metadata storage
const routes: Array<{
  method: string;
  path: string;
  handler: Function;
  controller: string;
}> = [];

// Controller decorator (class level)
function Controller(basePath: string) {
  return function (target: Function, context: ClassDecoratorContext) {
    (target as any).__basePath = basePath;
  };
}

// HTTP method decorators (method level)
function Get(path: string) {
  return function (
    originalMethod: Function,
    context: ClassMethodDecoratorContext
  ) {
    routes.push({
      method: "GET",
      path,
      handler: originalMethod,
      controller: String(context.name),
    });
    return originalMethod;
  };
}

function Post(path: string) {
  return function (
    originalMethod: Function,
    context: ClassMethodDecoratorContext
  ) {
    routes.push({
      method: "POST",
      path,
      handler: originalMethod,
      controller: String(context.name),
    });
    return originalMethod;
  };
}

// Usage
@Controller("/api/users")
class UserController {
  @Get("/")
  getAllUsers() {
    return [{ id: 1, name: "Alice" }];
  }

  @Get("/:id")
  getUserById(id: string) {
    return { id, name: "Alice" };
  }

  @Post("/")
  createUser(body: any) {
    return { id: 2, ...body };
  }
}

// routes array now contains all registered endpoints
console.log(routes);

This pattern is the foundation of how frameworks like NestJS, routing-controllers, and tsoa work. If you are building a REST API, our REST API Design Principles guide covers the design patterns that complement decorator-based routing.

TC39 Standard vs Legacy Decorators

FeatureTC39 Standard (TS 5.0+)Legacy (experimentalDecorators)
StatusStage 3 (stable)Deprecated path
tsconfig flagNone neededexperimentalDecorators: true
Class decoratorsYesYes
Method decoratorsYesYes
Property decoratorsVia accessor keywordDirect property decoration
Parameter decoratorsNot supportedYes
Metadata reflectionNot built inVia emitDecoratorMetadata

For new projects in 2026, use TC39 standard decorators unless your framework explicitly requires the legacy version. Angular is migrating to TC39 decorators, and NestJS has announced support as well. Check your framework's documentation for the current recommendation.

Performance Considerations

Decorators add a thin wrapper around the decorated element, which introduces a small runtime overhead. For most applications, this overhead is negligible. However, there are cases where you should be mindful of performance.

  • Hot paths -- Avoid decorators on methods that are called thousands of times per second in tight loops. The wrapper function call adds up.
  • Stacked decorators -- Each decorator adds another function call in the chain. Five decorators on one method means five extra function calls per invocation.
  • Initialization cost -- Class decorators run once when the class is defined, not on each instantiation. This is a one-time cost that is almost always acceptable.
  • Memoization and caching decorators -- These typically improve performance overall because the cache hit avoids the expensive computation. Just be careful about memory growth.

To measure the impact of decorators on your application, profile your code using Chrome DevTools or Node.js --prof flag. See our Web Performance Checklist for more profiling techniques. For quick code formatting before profiling, use the JavaScript Beautifier.

Best Practices for Using Decorators

  1. Keep decorators focused -- Each decorator should do one thing well. Use composition instead of building monolithic decorators.
  2. Name decorators clearly -- Use descriptive names like @Cacheable, @Validate, @Authorize that make their purpose obvious at a glance.
  3. Document execution order -- When multiple decorators are stacked, document which order they run in and any dependencies between them.
  4. Use decorator factories for configuration -- If a decorator needs parameters, always use the factory pattern: @Cache(60) rather than magic defaults.
  5. Test decorators in isolation -- Write unit tests for decorator functions independently of the classes they decorate.
  6. Prefer TC39 standard -- For new codebases, use the standardized decorator syntax. Only use legacy decorators when required by your framework.
  7. Avoid side effects in decorators -- Decorators that modify global state or make network calls are hard to test and reason about. Keep side effects in the method body.

Format Your TypeScript Code

Keep your TypeScript code clean and readable. Use our free JavaScript Beautifier to format TypeScript, JavaScript, and JSX code with consistent indentation and style.

Open JS Beautifier

Related Articles