Commissioned for this museum · after Gamma, Helm, Johnson & Vlissides, 1994
Decorator
TypeScript·1994·32 lines·890 bytes
/**
* Decorator — wrap a thing in another thing of the same shape.
*/
type Fetcher = (url: string) => Promise<string>;
const plain: Fetcher = async (url) => `<${url}>`;
// Each decorator takes a Fetcher and returns a Fetcher. Because the type
// going in and the type coming out are identical, they compose in any order
// and to any depth — nothing downstream can tell how many there are.
const withLogging =
(next: Fetcher): Fetcher =>
async (url) => {
console.log(`fetching ${url}`);
return next(url);
};
const withCache = (next: Fetcher): Fetcher => {
const seen = new Map<string, string>();
return async (url) => {
const hit = seen.get(url);
if (hit !== undefined) return hit;
const value = await next(url);
seen.set(url, value);
return value;
};
};
const fetcher = withLogging(withCache(plain));
await fetcher("/singleton");
await fetcher("/singleton");Curator’s note
The whole pattern is one type signature repeated: Fetcher in, Fetcher out. Everything else follows from that. Because what comes out is indistinguishable from what went in, decorators stack to any depth, in any order, and no caller ever learns how many are present.
Run the demonstration and the order announces itself. Two calls to the same URL log twice but only fetch once — because logging is on the outside of the cache. Swap the two and you get one log line and one fetch, because the cache now answers before logging is ever reached. The pattern gives you composition; it does not give you a right answer about what to compose. That is a decision someone has to make on purpose, and it is invisible in the type.
Notice that withCache closes over a Map created when the decorator is applied, not when it is called. That one line is the difference between a single cache shared by every request and a fresh empty cache on each one, and it is the bug people actually write. The type system is perfectly happy either way.
The 1994 book drew this as a class implementing an interface and holding a reference to another instance of that interface. A function returning a function of the same type is the same idea with the ceremony removed — which is why decorators are everywhere in languages with closures, and why nobody there calls them a pattern any more.