Skip to content
Skip to content
DocsTypeScript ExpertexpertDecorators
Chapter 8 of 10·expert·11 min read

Decorators

Decorator

Class, method, and property decorators for meta-programming

Hover or tap any paragraph to see Vietnamese translation

What Are Decorators?

A decorator is a meta-programming feature that lets you attach annotations and executable logic to classes, methods, properties, and parameters. They use the @expression syntax and run when the decorated declaration is evaluated — not when an instance is constructed.

Enabling Decorators: TS 5.x vs Legacy

TypeScript 5.0 implements the Stage 3 ECMAScript Decorator Proposal with no tsconfig flag required. Older versions use experimentalDecorators, which is a different, incompatible API still required by frameworks like NestJS and TypeORM.

tsconfig.json
1// tsconfig.json — TypeScript 5.0+ (Stage 3, current standard)2{3  "compilerOptions": {4    "target": "ES2022",5    "lib": ["ES2022", "DOM"]6    // No experimentalDecorators flag needed7  }8}910// tsconfig.json — Legacy decorators (NestJS, TypeORM, Inversify)11{12  "compilerOptions": {13    "target": "ES5",14    "experimentalDecorators": true,15    "emitDecoratorMetadata": true  // required for Reflect.metadata16  }17}
Info
Do not mix both modes in the same project. Check your framework first: if you are using NestJS, stay on legacy mode until it officially migrates to Stage 3.

Class Decorators

A class decorator receives the target class and a ClassDecoratorContext. It can return a new class to replace the original, mutate the class in place and return void, or use context.addInitializer to run logic after the class is defined.

class-decorator-sealed.ts
1// Sealing a class — prevents new properties being added at runtime2function sealed<T extends abstract new (...args: unknown[]) => unknown>(3  target: T,4  _context: ClassDecoratorContext5): T {6  Object.seal(target);7  Object.seal(target.prototype);8  return target;9}1011@sealed12class Config {13  host = "localhost";14  port = 3000;15}1617// Config.prototype.newMethod = () => {}; // Runtime TypeError
class-decorator-singleton.ts
1// Singleton — ensures only one instance can ever be created2function singleton<T extends new (...args: unknown[]) => object>(3  target: T,4  _context: ClassDecoratorContext5): T {6  let instance: InstanceType<T> | undefined;78  return new Proxy(target, {9    construct(ctor, args) {10      if (!instance) {11        instance = Reflect.construct(ctor, args) as InstanceType<T>;12      }

Method Decorators

A method decorator receives the original method and a ClassMethodDecoratorContext. To intercept calls, return a replacement function — TypeScript infers the correct type automatically from the target.

method-decorator-log.ts
1function log<This, Args extends unknown[], Return>(2  target: (this: This, ...args: Args) => Return,3  context: ClassMethodDecoratorContext<This, typeof target>4): typeof target {5  const name = String(context.name);6  return function (this: This, ...args: Args): Return {7    console.log(`[${name}] called with`, args);8    const result = target.call(this, ...args);9    console.log(`[${name}] returned`, result);10    return result;11  };12}
method-decorator-measure.ts
1// Timing decorator — async-aware performance measurement2function measure<This, Args extends unknown[], Return>(3  target: (this: This, ...args: Args) => Return,4  context: ClassMethodDecoratorContext<This, typeof target>5): typeof target {6  const name = String(context.name);7  return function (this: This, ...args: Args): Return {8    const t0 = performance.now();9    const result = target.call(this, ...args);10    const duration = performance.now() - t0;11    console.log(`${name}: ${duration.toFixed(2)}ms`);12    return result;

Property and Accessor Decorators

Field decorators in TS 5.x receive undefined as their target because the field does not exist when the decorator runs. They return an initializer function that transforms the initial value at construction time. Accessor decorators use the accessor keyword to intercept both get and set.

field-decorator.ts
1function nonEmpty(2  _target: undefined,3  context: ClassFieldDecoratorContext4) {5  const name = String(context.name);6  return function (this: object, value: string): string {7    if (typeof value !== "string" || value.trim() === "") {8      throw new TypeError(`${name} cannot be empty`);9    }10    return value;11  };12}
accessor-decorator.ts
1// 'accessor' creates a backing private field + getter + setter2// enabling ClassAccessorDecoratorContext interception3function clamp(min: number, max: number) {4  return function <T>(5    target: ClassAccessorDecoratorTarget<T, number>,6    _context: ClassAccessorDecoratorContext<T, number>7  ): ClassAccessorDecoratorResult<T, number> {8    return {9      get(this: T): number {10        return target.get.call(this);11      },12      set(this: T, value: number): void {

Parameter Decorators (Legacy Only)

Parameter decorators are not part of the Stage 3 spec and only work with experimentalDecorators: true. They are the backbone of DI frameworks like NestJS and Inversify, marking constructor parameters for injection.

parameter-decorator.ts
1// Requires: experimentalDecorators: true, emitDecoratorMetadata: true2import "reflect-metadata";34const INJECT_TOKENS = Symbol("inject");56function inject(token: string) {7  return function (8    target: object,9    _propertyKey: string | symbol | undefined,10    parameterIndex: number11  ): void {12    const tokens: string[] =

Decorator Factories

A decorator factory is a function that returns a decorator, allowing you to pass configuration arguments. This is the dominant pattern in production code — nearly every real-world decorator is a factory.

decorator-factory-retry.ts
1function retry(maxAttempts: number, delayMs = 100) {2  return function <This, Args extends unknown[], Return>(3    target: (this: This, ...args: Args) => Promise<Return>,4    context: ClassMethodDecoratorContext<This, typeof target>5  ) {6    const name = String(context.name);7    return async function (this: This, ...args: Args): Promise<Return> {8      let lastError: unknown;9      for (let attempt = 1; attempt <= maxAttempts; attempt++) {10        try {11          return await target.call(this, ...args);12        } catch (err) {

Decorator Composition

When multiple decorators are stacked, their factory functions run top-to-bottom, but the decorators themselves are applied bottom-to-top — the decorator closest to the declaration runs first.

decorator-composition-order.ts
1function outer(_t: Function, _c: ClassDecoratorContext) {2  console.log("outer applied");3}45function inner(_t: Function, _c: ClassDecoratorContext) {6  console.log("inner applied");7}89@outer   // factory called 1st, decorator applied 2nd10@inner   // factory called 2nd, decorator applied 1st11class Example {}1213// Output:14// inner applied15// outer applied
decorator-composition-practical.ts
1// Practical: memoize + readonly composed on a method2function memoize<This extends object, Args extends unknown[], Return>(3  target: (this: This, ...args: Args) => Return,4  _context: ClassMethodDecoratorContext5): typeof target {6  const cache = new Map<string, Return>();7  return function (this: This, ...args: Args): Return {8    const key = JSON.stringify(args);9    if (cache.has(key)) return cache.get(key)!;10    const result = target.call(this, ...args);11    cache.set(key, result);12    return result;
Info
Application order matters: @readonly @memoize means memoize transforms the original method first, then readonly wraps the already-memoized result. Reversing the order changes behaviour.

Summary

  • TypeScript 5.0+ implements Stage 3 decorators — no tsconfig flags required
  • Class decorators receive the target class and context; return a new class to replace or void to mutate in place
  • Method decorators return a replacement function with the same signature to intercept calls
  • Field decorators return an initializer function to transform the initial value at construction time
  • Use the accessor keyword to unlock accessor decorators — enabling interception of both get and set
  • Parameter decorators are legacy-only and used by DI frameworks such as NestJS and Inversify
  • A decorator factory is a function that returns a decorator — the standard way to pass configuration arguments
  • Decorators are applied bottom-to-top; their factory functions are called top-to-bottom
  • Built: 6/25/2026, 3:03:36 PM