Decorators

Annotate and extend classes, methods, and fields with reusable decorators

Overview

Decorators are functions that attach reusable behavior or metadata to classes and their members. TypeScript 5 implements the ECMAScript Stage 3 decorator proposal, where a decorator receives the target and a context object and may return a replacement. They are widely used in frameworks for dependency injection, routing, and validation.

Syntax / Usage

A class-method decorator receives the original method and a ClassMethodDecoratorContext, returning a new function that wraps it.

function logged<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This>
) {
  const name = String(context.name)
  return function (this: This, ...args: Args): Return {
    console.log(`calling ${name}`)
    return target.apply(this, args)
  }
}

class Service {
  @logged
  fetch(id: number) {
    return { id }
  }
}

Examples

A class decorator can seal the constructor and its prototype.

function sealed(target: Function, _context: ClassDecoratorContext) {
  Object.seal(target)
  Object.seal(target.prototype)
}

@sealed
class Registry {}

Use context.addInitializer from a field decorator to bind a method.

function bound<This, V extends (...args: any[]) => any>(
  _target: undefined,
  context: ClassFieldDecoratorContext<This, V>
) {
  context.addInitializer(function (this: This) {
    const self = this as Record<string, unknown>
    self[context.name as string] = (self[context.name as string] as V).bind(this)
  })
  return undefined
}

An auto-accessor decorator can validate assignments through set.

function positive(_target: ClassAccessorDecoratorTarget<unknown, number>, _ctx: ClassAccessorDecoratorContext) {
  return {
    set(this: unknown, value: number) {
      if (value < 0) throw new RangeError("must be positive")
    },
  }
}

Common Mistakes

  • Mixing the legacy experimentalDecorators API with the Stage 3 signatures
  • Assuming decorators run per call—they run once at class definition time
  • Forgetting to return the wrapped method, so the original is not replaced
  • Reading context.name as a string without String(...), since it may be a symbol
  • Relying on parameter decorators, which are not part of the Stage 3 standard

See Also

typescript-declaration-files typescript-mapped-types interfaces