Advanced0 questionsFull Guide

TypeScript Decorators — Complete Interview Guide

Deep-dive into TypeScript decorators — class, method, property, accessor, and parameter decorators. Understand the execution order, decorator factories, metadata reflection, and how frameworks like NestJS, Angular, and MobX are built on top of this feature.

The Mental Model

Decorators are annotations that wrap or enhance a class, method, property, or parameter at decoration time — similar to Python decorators or Java annotations, but with the ability to modify the target. Think of them as gift-wrapping functions: you pass in the original class or method, you add your enhancement layer around it (logging, validation, caching, dependency injection), and the consumer interacts with the enhanced version without knowing the original is wrapped inside. The critical thing to understand is that decorators run at class definition time (when the module loads), not at instance creation time — this determines what they can and cannot do.

The Explanation

Decorator Basics and Configuration

Decorators are currently a Stage 3 TC39 proposal. TypeScript supports both the legacy experimental decorators (widely used, requires experimentalDecorators: true) and the newer Stage 3 decorators. Most production frameworks (NestJS, Angular, MobX) use the legacy form. This guide covers the legacy form since it dominates interview contexts:

// tsconfig.json — required for legacy decorators
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // required for reflect-metadata
  }
}

// A decorator is a function — it receives the decorated target as its argument
function Log(target: Function) {
  console.log(`Class defined: ${target.name}`);
}

@Log
class UserService {
  // Log runs at class definition time, once, when this module loads
  // NOT when new UserService() is called
}

Class Decorators

// Class decorator — receives the constructor function
// Can return a new constructor to replace the original class
type Constructor<T = {}> = new (...args: any[]) => T;

function Singleton<T extends Constructor>(Base: T) {
  let instance: InstanceType<T> | null = null;
  return class extends Base {
    constructor(...args: any[]) {
      if (instance) return instance as any;
      super(...args);
      instance = this as any;
    }
  };
}

@Singleton
class DatabaseConnection {
  connect() { /* ... */ }
}

const a = new DatabaseConnection();
const b = new DatabaseConnection();
console.log(a === b); // true — same instance

Method Decorators

// Method decorator receives: target (prototype), key (method name), descriptor
function Memoize(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  const original = descriptor.value;
  const cache = new Map<string, any>();

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

  return descriptor;
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

// Logging decorator — a real-world pattern
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${key} with`, args);
    const result = original.apply(this, args);
    console.log(`${key} returned`, result);
    return result;
  };
  return descriptor;
}

Decorator Factories

A decorator factory is a function that returns a decorator — allowing the decorator to be parameterized:

// Factory — outer function takes config, returns the actual decorator
function Throttle(ms: number) {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    let lastCall = 0;
    descriptor.value = function(...args: any[]) {
      const now = Date.now();
      if (now - lastCall >= ms) {
        lastCall = now;
        return original.apply(this, args);
      }
    };
    return descriptor;
  };
}

class SearchService {
  @Throttle(300)  // Decorator factory — passes 300 to the outer function
  search(query: string) { /* ... */ }
}

// NestJS uses decorator factories extensively:
// @Controller('users')
// @Get(':id')
// @Body(), @Param('id'), @Injectable()
// All of these are decorator factories returning the actual decorator

Property and Parameter Decorators

// Property decorator — receives target and property key
// Cannot directly modify the property (no descriptor provided)
// Typically used with reflect-metadata to record metadata
function Required(target: any, key: string) {
  const metadata: string[] = Reflect.getMetadata("required", target) || [];
  metadata.push(key);
  Reflect.defineMetadata("required", metadata, target);
}

// Parameter decorator — receives target, method name, and parameter index
function ValidateId(target: any, key: string, index: number) {
  // Marks the parameter at 'index' in method 'key' for validation
  const params: number[] = Reflect.getMetadata("validateId", target, key) || [];
  params.push(index);
  Reflect.defineMetadata("validateId", params, target, key);
}

class UserController {
  @Required
  name: string = "";

  getUser(@ValidateId id: string) {
    return users.find(u => u.id === id);
  }
}

Decorator Execution Order

// 1. Parameter decorators evaluated bottom-up, then
// 2. Method/accessor/property decorators evaluated bottom-up, then
// 3. Parameter decorators for constructor, then
// 4. Class decorator evaluated last

@ClassDec          // Runs 4th
class Example {
  @PropDec         // Runs 3rd
  value: string;

  @MethodDec1      // Runs 2nd (outer)
  @MethodDec2      // Runs 1st (inner — applied closer to method first)
  method(@ParamDec param: string) {}
}

// The general rule: innermost decorator runs first, class decorator runs last

Common Misconceptions

⚠️

Many developers think decorators run when instances are created — class and method decorators run at class definition time, when the module loads. They are applied once to the prototype or constructor, not once per instance. Only the wrapper function they install runs per call.

⚠️

Many developers think TypeScript decorators are stable and finalized — the legacy experimental decorators (experimentalDecorators: true) have been in TypeScript since v1.5 but are NOT the TC39 standard. The actual Stage 3 decorator proposal has a different API. Most frameworks still use the legacy form.

⚠️

Many developers think property decorators can intercept gets and sets directly — unlike method decorators, property decorators do not receive a PropertyDescriptor. To observe property access, you need to use Object.defineProperty inside the decorator or combine with reflect-metadata.

⚠️

Many developers think decorators are just syntactic sugar for higher-order functions applied manually — they are structurally equivalent, but they are applied at a specific phase in class evaluation, which determines the execution order and what targets are available.

⚠️

Many developers think emitDecoratorMetadata is optional — it is required to use reflect-metadata for dependency injection (the technique Angular and NestJS rely on to resolve constructor parameter types). Without it, Reflect.getMetadata('design:type') returns undefined.

⚠️

Many developers think decorators are only useful in NestJS and Angular — MobX uses decorators for observable and computed, TypeORM for Entity and Column, class-validator for validation rules, and Swagger for API documentation — all common across the TypeScript ecosystem.

Where You'll See This in Real Code

NestJS dependency injection: @Injectable(), @Controller('users'), @Get(':id'), and @Body() are all decorators. The framework reads decorator metadata at startup to wire up the dependency injection container, route handlers, and request parsing automatically.

Angular components: @Component({ selector, template, styles }) is a class decorator factory that registers the component with Angular's compiler and attaches its metadata — template, styles, change detection strategy — to the class.

MobX state management: @observable and @computed are property decorators that replace the property with a getter/setter pair backed by MobX's reactive system — every assignment triggers reactive updates without any manual subscription code.

class-validator: @IsEmail(), @MinLength(8), @IsOptional() are property decorators that store validation rules as metadata. The validate(instance) function reads that metadata at runtime to perform all validations in one call.

TypeORM entities: @Entity(), @Column(), @PrimaryGeneratedColumn(), @ManyToOne() are decorators that describe the database schema in terms of TypeScript classes — TypeORM reads the metadata to generate migrations and build queries.

Swagger/OpenAPI documentation: @ApiProperty(), @ApiTags(), @ApiResponse() from @nestjs/swagger automatically generate OpenAPI specs from decorated controllers and DTOs — zero manual YAML or JSON needed.

Interview Cheat Sheet

  • Enable: experimentalDecorators: true in tsconfig.json; emitDecoratorMetadata: true for reflect-metadata
  • Class decorator: receives the constructor; can return a new constructor to replace the class
  • Method decorator: receives (target, key, descriptor); modify descriptor.value to wrap the method
  • Property decorator: receives (target, key); no descriptor — use reflect-metadata to store metadata
  • Parameter decorator: receives (target, key, parameterIndex); typically stores metadata for later validation
  • Decorator factory: outer function with config returns the actual decorator — enables @Throttle(300)
  • Execution order: parameter → method/property decorators (bottom-up) → class decorator (last)
  • Decorators run at class definition time, not at instance creation time
  • Legacy experimental decorators vs Stage 3 decorators: different APIs, most frameworks use legacy form
  • reflect-metadata: needed for design:type, design:paramtypes metadata used by DI containers
💡

How to Answer in an Interview

  • 1.Lead with the runtime timing distinction: 'Decorators run at class definition time — when the module loads. They are applied to the prototype or constructor once. The functions they install run at call time, but the decoration itself is a one-time class-load operation.'
  • 2.Connect to framework usage: 'I use decorators daily through NestJS — @Injectable(), @Controller(), @Get() — but understanding how they work lets me write custom decorators for cross-cutting concerns like logging, caching, and authorization guards.'
  • 3.The decorator factory pattern is important to explain: 'A plain decorator takes the target directly. A decorator factory is a function that receives config and returns a decorator — that's why @Throttle(300) has parentheses. The outer function runs immediately to close over 300, then the returned decorator is applied to the method.'
  • 4.Be clear about the state of decorators: 'The legacy experimentalDecorators form is what most frameworks use today. There is a newer Stage 3 TC39 proposal with a different API. TypeScript supports both, but they are not compatible — this is an important distinction in job settings that care about standards compliance.'

Practice Questions

No questions tagged to this topic yet.

Related Topics

TypeScript Mapped Types — Complete Interview Guide
Advanced·6–10 Qs
TypeScript Classes & Access Modifiers — Complete Interview Guide
Intermediate·6–10 Qs
TypeScript Generics — Complete Interview Guide
Intermediate·8–12 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz