Skip to content
Decorators
step 1/7

Reading — step 1 of 7

Learn

~3 min readThe Type System Toolkit

Decorators add metadata or behavior to classes, methods, properties, and parameters at definition time. After years as a stage-2 proposal, decorators stabilized as stage-3 in 2022 and TypeScript 5.0 ships the new spec-compliant form.

Note: Older Angular/NestJS code uses the LEGACY decorator syntax (TS 4.x with experimentalDecorators: true). Modern code in TS 5.0+ uses the STAGE-3 form. They differ.

Stage-3 decorators (TS 5.0+)

A class method decorator:

function logged(
    target: (this: any, ...args: any[]) => any,
    context: ClassMethodDecoratorContext
): typeof target {
    return function (...args: any[]) {
        console.log(`calling ${String(context.name)} with`, args);
        const result = target.apply(this, args);
        console.log(`returned`, result);
        return result;
    };
}

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

new Calculator().add(2, 3);
// calling add with [ 2, 3 ]
// returned 5

The decorator function receives:

  • target — the original method
  • context — metadata about the decoration (kind, name, addInitializer, etc.)

Returns a replacement method (or undefined to keep the original).

Class decorator

function sealed<T extends new (...args: any[]) => any>(
    target: T,
    _context: ClassDecoratorContext
): T {
    Object.seal(target);
    Object.seal(target.prototype);
    return target;
}

@sealed
class Frozen { /* ... */ }

Class decorators receive the constructor and can return a replacement (or modify in place).

Field decorator

function lowercase(
    _value: undefined,
    context: ClassFieldDecoratorContext
): (this: any, value: string) => string {
    return function (value: string) {
        return value.toLowerCase();
    };
}

class Email {
    @lowercase
    address: string;
    
    constructor(addr: string) { this.address = addr; }
}

new Email('[email protected]').address;     // '[email protected]'

Field decorators return an INITIALIZER function that's called with the assigned value and returns the actual stored value.

Decorator factories — adding parameters

For decorators that take config:

function min(limit: number) {
    return function(
        target: any,
        context: ClassFieldDecoratorContext
    ) {
        return function(value: number) {
            if (value < limit) throw new RangeError(`${String(context.name)} < ${limit}`);
            return value;
        };
    };
}

class Order {
    @min(0)
    quantity: number;
}

The outer function takes args; the inner is the actual decorator.

context.addInitializer

Lets a decorator register code to run once per instance, after construction:

function bound<T extends (...args: any[]) => any>(
    method: T,
    context: ClassMethodDecoratorContext
) {
    context.addInitializer(function() {
        (this as any)[context.name] = method.bind(this);
    });
}

class Counter {
    count = 0;
    
    @bound
    increment() {
        this.count++;
    }
}

const c = new Counter();
const inc = c.increment;       // detached
inc();                           // still updates c.count — bound this

Useful for auto-binding methods (no more .bind(this) in constructors).

Legacy vs stage-3 — quick reference

Legacy (experimentalDecorators)Stage-3 (TS 5.0+)
Method decorator signature(target, key, descriptor)(method, context)
Class decorator signature(constructor)(constructor, context)
Property decoratorYes (no value access)Replaced by accessor / field decorators
Parameter decoratorYesNOT in stage-3 (yet)
reflect-metadata integrationCommonNot yet — stage-3 metadata proposal in flight

Most framework code today (NestJS, Angular, TypeORM) is still on the legacy form — they'll migrate as the new spec settles.

When to use decorators

Good fits:

  • Cross-cutting concerns: logging, validation, caching
  • Framework hooks: dependency injection, route registration, ORM mapping
  • Method binding (@bound)
  • Memoization

Bad fits:

  • Anything you could express with composition or higher-order functions
  • Hot-path code (decorators add overhead per call)
  • Anything that should be visible in source — decorators hide behavior

Decorators feel magical. That's a strength for frameworks; a risk for application code.

Common mistakes

  • Mixing legacy and stage-3 decorators in one project — the syntax overlaps but the semantics differ. Pick one.
  • Forgetting that legacy decorators need experimentalDecorators: true in tsconfig.
  • Assuming reflect-metadata works with stage-3 — it doesn't (yet). The stage-3 metadata proposal is separate.
  • Side effects in decorators — they run at class-definition time, BEFORE main(). Be careful.
  • Decorators on private fields — they don't see #name fields. Use regular fields or accessors.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…