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?