Master

Decorators and Metaprogramming

Add behavior to classes and methods with decorators and descriptors.

What you will see

connected

About 2 min of reading

Try It

Wrap a method with extra behavior the way a decorator would, cache its results by argument, and parameterize the wrapping with a factory. Each step replaces the original while preserving it.

1

Replace a method descriptor with a wrapper that logs before delegating. Call the method once and watch the log line appear, then the real result.

function log(target, key, descriptor) { const original = descriptor.value; descriptor.value = function (...args) { console.log(`logging call to ${key}`); return original.apply(this, args); }; return descriptor; } class Calculator { add(a, b) { return a + b; } } Object.defineProperty(Calculator.prototype, 'add', log(Calculator.prototype, 'add', Object.getOwnPropertyDescriptor(Calculator.prototype, 'add'))); console.log(new Calculator().add(2, 3));

Before you run it, say out loud what it will print. Then open the answer.

What it prints
logging call to add 5
A decorator wraps a member by replacing its descriptor. The descriptor holds the original, so you can preserve it while adding behavior.
2

Memoize an expensive method by caching results on argument signatures. Call it twice with the same input and confirm the wrapper returns the cached value.

function memoize(target, key, descriptor) { const fn = descriptor.value; const cache = new Map(); descriptor.value = function (...args) { const k = JSON.stringify(args); if (cache.has(k)) return cache.get(k); const result = fn.apply(this, args); cache.set(k, result); return result; }; return descriptor; } class MathOps { square(n) { return n * n; } } Object.defineProperty(MathOps.prototype, 'square', memoize(MathOps.prototype, 'square', Object.getOwnPropertyDescriptor(MathOps.prototype, 'square'))); console.log(new MathOps().square(4)); console.log(new MathOps().square(4));

Before you run it, say out loud what it will print. Then open the answer.

What it prints
16 16
A method decorator can replace the function entirely. Caching by argument signature turns repeated expensive calls into O(1) lookups.
3

Build a factory decorator that retries a method a fixed number of times before giving up. The wrapped method fails twice, then succeeds on the third attempt.

function retry(times) { return function (target, key, descriptor) { const fn = descriptor.value; descriptor.value = function (...args) { for (let i = 0; i < times; i++) { try { return fn.apply(this, args); } catch (e) { if (i === times - 1) throw e; } } }; return descriptor; }; } let attempts = 0; class Service { connect() { attempts++; if (attempts < 3) throw new Error("fail"); return "connected"; } } Object.defineProperty(Service.prototype, 'connect', retry(3)(Service.prototype, 'connect', Object.getOwnPropertyDescriptor(Service.prototype, 'connect'))); console.log(new Service().connect());

Before you run it, say out loud what it will print. Then open the answer.

What it prints
connected
A factory decorator returns the actual decorator, letting you parameterize behavior. It scales to logging, validation, and error handling.

Data Flow

A decorator wraps a member at class-definition time, replacing its descriptor with a version that adds behavior.
1 Class is defined -> decorators run
↓
2 Decorator receives target, key, descriptor
↓
3 Replace or augment the descriptor
↓
4 Wrapped member runs on every access/call
Why this works
The decorator lifecycle

Modern decorators run at class definition time, before instances exist:

class A {
  @decorator field;      // called during class creation
  @methodDecorator fn() {}
}

Class decorators receive the constructor; method decorators receive the class, key, and property descriptor; field decorators receive the constructor and key. They must return a descriptor for members that have one.

When you have a choice

Descriptive vs functional

You can build reusable decorators as factories:

function retry(times) {
  return function (target, key, descriptor) {
    const fn = descriptor.value;
    descriptor.value = function (...args) {
      for (let i = 0; i < times; i++) {
        try { return fn.apply(this, args); }
        catch (e) { if (i === times - 1) throw e; }
      }
    };
  };
}

A factory decorator returns the actual decorator, letting you parameterize behavior. This pattern scales to logging, validation, auth, and error handling.

Examples

Class field descriptor

Logging property access

function log(target, key, descriptor) {
  const original = descriptor.get;
  descriptor.get = function () {
    console.log(`reading ${key}`);
    return original.call(this);
  };
  return descriptor;
}

class Account {
  @log balance = 100;
}
console.log(new Account().balance);   // logs 'reading balance', then 100

Decorators wrap a member with extra behavior. The descriptor holds the getter/setter/value, so you can replace it while preserving the original.

Method decorator with caching

Memoizing expensive calls

function memoize(target, key, descriptor) {
  const fn = descriptor.value;
  const cache = new Map();
  descriptor.value = function (...args) {
    const k = JSON.stringify(args);
    if (cache.has(k)) return cache.get(k);
    const result = fn.apply(this, args);
    cache.set(k, result);
    return result;
  };
}

A method decorator can replace the function entirely. Here it caches results by argument signature, turning repeated expensive calls into O(1) lookups.

Parameter decorators

Validating inputs

function positive(target, key, parameterIndex) {
  // records that this parameter must be positive
}
class Calculator {
  add(@positive amount) { return amount; }
}

Parameter decorators receive the target, the member name, and the parameter's position. They are commonly paired with a registry that wraps the method to enforce rules at call time.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Proxy and Reflect
Previous Lesson
Proxy and Reflect
Next Lesson
Advanced Functional Patterns
Advanced Functional Patterns →