All posts
typescriptdecorators

TypeScript Decorators: A Practical Guide for Full-Stack Developers

A practical guide to TypeScript decorators — class, method, and property decorators, the new Stage 3 spec, and when to actually use them.

SR

Suhail Roushan

August 6, 2026

·
4 min read
·
0 views

TypeScript decorators let you attach behavior to a class, method, or property with a single @ symbol, and if you've used NestJS or Angular, you've been using them without necessarily knowing how they work.

TypeScript decorators are functions that run at class-definition time and can observe, modify, or replace the class, method, property, or parameter they're attached to. They're not unique to TypeScript — decorators are now a Stage 3 ECMAScript proposal, and TypeScript 5.0+ supports the modern spec-compliant version, which is different enough from the old experimentalDecorators behavior that it's worth understanding both.

Why Decorators Matter (and When to Skip Them)

Decorators exist to solve cross-cutting concerns cleanly — logging, validation, dependency injection, caching — without repeating the same wrapper logic in every method. Frameworks like NestJS lean on them heavily for exactly this reason: @Controller(), @Get(), and @Injectable() all use decorators to keep route definitions declarative.

Skip decorators if you're not already using a framework built around them. For plain application code, a decorator adds a layer of indirection that a regular higher-order function often handles more transparently.

Getting Started with Decorators

Enable them in tsconfig.json (for the modern spec-compliant version, no flag is needed in TypeScript 5.0+ with target: ES2022 or higher; for the legacy version you still need experimentalDecorators: true):

{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": false
  }
}

A minimal method decorator that logs every call:

function logCall(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  function replacementMethod(this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with`, args);
    return originalMethod.call(this, ...args);
  }
  return replacementMethod;
}

class OrderService {
  @logCall
  createOrder(id: string) {
    return { id, status: "created" };
  }
}

Core Decorator Concepts Every Developer Should Know

Class decorators run once, when the class is defined, and can replace the constructor entirely — useful for things like automatically registering a class in a DI container.

Method decorators wrap the method implementation, exactly like the logCall example above. This is the most common decorator type in real codebases — validation, retries, and permission checks are all natural fits.

Property and accessor decorators intercept reads/writes to a field. NestJS's @Injectable() metadata and validation libraries like class-validator's @IsEmail() are built on this pattern.

Decorator factories. Most real decorators aren't bare functions — they're functions that return a decorator, so you can pass configuration:

function retry(times: number) {
  return function (originalMethod: any, context: ClassMethodDecoratorContext) {
    return async function (this: any, ...args: any[]) {
      let lastError: unknown;
      for (let i = 0; i < times; i++) {
        try {
          return await originalMethod.call(this, ...args);
        } catch (err) {
          lastError = err;
        }
      }
      throw lastError;
    };
  };
}

class PaymentService {
  @retry(3)
  async chargeCard(amount: number) {
    // ...
  }
}

Common Decorator Mistakes and How to Fix Them

Mistake 1: mixing legacy and modern decorator syntax. The pre-5.0 experimentalDecorators API and the Stage 3 spec-compliant one have different signatures — a decorator written for one will silently misbehave under the other. Fix: check which mode your tsconfig.json uses before copying decorator code from older tutorials or Stack Overflow answers.

Mistake 2: decorators that swallow errors silently. A retry or logging decorator that catches an exception and doesn't rethrow makes debugging production issues nearly impossible, because the stack trace never surfaces. Fix: always rethrow after logging, or clearly document that a decorator suppresses errors.

Mistake 3: overusing decorators for simple logic. Wrapping a single-line function in a decorator for something that a plain if statement handles just as well adds cognitive overhead for no real benefit. Fix: reach for decorators when the same cross-cutting logic repeats across three or more methods, not for one-off cases.

When Should You Use Decorators?

Use them when you're already inside a framework that expects them (NestJS, TypeORM, Angular) or when the same wrapper logic — logging, auth checks, caching — repeats across many methods in a class-based codebase. Skip them in functional-style codebases where a higher-order function does the same job with less magic.

Decorators in Production

Metadata reflection (reflect-metadata) pairs with decorators in frameworks like NestJS to enable runtime type inspection for dependency injection — if you're building your own decorator-based system, that library is worth knowing about. Also test decorators in isolation: because they run at class-definition time, a bug in a decorator factory can break every class that uses it, so unit test the decorator function directly, not just the classes that consume it.

Before reaching for a decorator, check whether you're already in a framework that expects one — outside that context, a plain wrapper function is usually the simpler, more debuggable choice.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch