๐ŸŽ“ A1Learner
๐Ÿ”
0 of 37 lessons completed

TypeScript Decorator Patterns

Decorator patterns wrap methods and accessors with reusable behaviour in the stage-3 form. Each pattern keeps the decorated element working with its original types.

A logging decorator keeps the method types

The decorator is generic over the receiver, the arguments, and the result. The wrapped methods keep their types, so the assignments below carry number and string.

function logged<This, Args extends unknown[], Result>(
  value: (this: This, ...args: Args) => Result,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>
) {
  return function (this: This, ...args: Args): Result {
    console.log('call ' + String(context.name));
    const result = value.call(this, ...args);
    console.log('return ' + String(result));
    return result;
  };
}

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

  @logged
  shout(word: string): string {
    return word.toUpperCase();
  }
}

const calc = new Calculator();
const total: number = calc.add(2, 3);
const loud: string = calc.shout('hey');
console.log(total);
console.log(loud);

A validation decorator checks arguments first

The decorator inspects the quantity before the original method runs. A failed check returns a rejection, and the original method never runs.

function positive(value: (quantity: number) => string, context: ClassMethodDecoratorContext) {
  return function (this: unknown, quantity: number): string {
    if (quantity <= 0) {
      return 'rejected: quantity must be above zero';
    }
    return value.call(this, quantity);
  };
}

class OrderDesk {
  @positive
  place(quantity: number): string {
    return 'placed ' + quantity + ' items';
  }
}

const desk = new OrderDesk();
console.log(desk.place(3));
console.log(desk.place(0));

A lazy accessor computes once

The decorator caches the stored value per instance on the first read. The computed line prints once, even though the heading is read twice.

function lazy<This extends object, Value>(
  value: { get(this: This): Value; set(this: This, next: Value): void },
  context: ClassAccessorDecoratorContext<This, Value>
) {
  const cache = new WeakMap<object, Value>();
  return {
    get(this: This): Value {
      if (cache.has(this)) {
        return cache.get(this) as Value;
      }
      const result = value.get.call(this);
      cache.set(this, result);
      console.log('computed ' + String(context.name));
      return result;
    },
    set(this: This, next: Value): void {
      value.set.call(this, next);
      cache.set(this, next);
    },
  };
}

class DigestCard {
  @lazy
  accessor heading: string = 'Weekly digest';
}

const card = new DigestCard();
console.log(card.heading);
console.log(card.heading);
card.heading = 'Monthly digest';
console.log(card.heading);

Example

function logged<This, Args extends unknown[], Result>(
  value: (this: This, ...args: Args) => Result,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>
) {
  return function (this: This, ...args: Args): Result {
    console.log('call ' + String(context.name));
    const result = value.call(this, ...args);
    console.log('return ' + String(result));
    return result;
  };
}

function positive(value: (seats: number) => string, context: ClassMethodDecoratorContext) {
  return function (this: unknown, seats: number): string {
    if (seats <= 0) {
      return 'rejected: seats must be above zero';
    }
    return value.call(this, seats);
  };
}

class BookingDesk {
  @logged
  @positive
  reserve(seats: number): string {
    return 'reserved ' + seats + ' seats';
  }
}

const desk = new BookingDesk();
console.log(desk.reserve(2));
console.log(desk.reserve(0));

Key points

  • A logging decorator can stay generic over the method signature, so the wrapped method keeps its parameter and return types.
  • A validation decorator checks the arguments before the original method runs.
  • A validation decorator can reject a call by returning a result without calling the original method.
  • A lazy accessor decorator reads the stored value once and caches it, so later reads return the cached value.
  • Writing through a lazy accessor updates both the stored value and the cache.

In this example, the logging decorator wraps the validation decorator, which wraps the reserve method. The valid call passes the check and runs, and the invalid call returns the rejection, with both lines logged either way.

Note

Every pattern in this lesson uses the stage-3 form, so no compiler flag is needed.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท TypeScript Playground

Exercise

What does the lazy accessor decorator do on the second read of the value?