Summary — The Patterns Without a Chapter, and the Full List of Judgments
This chapter covers the patterns that got no chapter of their own in brief, then collects the whole series' judgments into one table.
The eight patterns without a chapter
All of them are catalogued in GoF1. They got no chapter of their own because the range of discussion is narrow from a TypeScript angle, because the material overlaps with another chapter, or because an existing article covers them in a different context. This selection, and the "verdict" in each short take below, are this guide's editorial judgment. Unlike the verdicts in the full chapters, these do not come with what would overturn them.
Prototype
A pattern that creates a new object by copying an existing one. There is a standard API for deep copying.
The
structuredClone()method of theWindowinterface creates a deep clone of a value using the structured clone algorithm.2
The quotation is from a page about the browser's Window, but Node.js has had a global function of the same name since v17.0.03.
For copying plain data, that is all you need. It does not work on class instances, though. The specification states the limits explicitly.
The prototype chain is not walked or duplicated.4
Property descriptors, setters, getters, and similar metadata-like features are not duplicated.4
These limits do not appear in the types, which calls for care.
class SavedArticle {
constructor(readonly url: string) {}
get host(): string {
return new URL(this.url).host;
}
}
const original = new SavedArticle('https://example.com/a');
const copy = structuredClone(original);
// The type is SavedArticle, but at runtime it is a plain object.
// copy instanceof SavedArticle is false, and copy.host is undefined
const stillTyped: string = copy.host;
The type of structuredClone returns the input type unchanged, so the compiler warns about nothing. To copy a class instance, you end up writing clone() yourself. Verdict: plain data is covered by the standard API; a class instance needs a copy you write yourself.
Facade
A pattern that gives a large set of parts a simple entry point. In TypeScript, within a single module, whatever you do not export is untouchable from outside, so what you make public is itself the entry point. Across several modules, it becomes a matter of placing one module that re-exports only what you want public. That only consolidates the entry point, though; it does not block the path of importing the inner modules directly. Blocking that takes something outside the language, such as a lint rule. Verdict: if all you need is a consolidated entry point, the module boundary covers it without standing up a Facade class.
Command
A pattern that bundles an operation together with its arguments into an object so it can be carried around. If you only need to call it, a closure is enough. Undo and queueing are also writable by lining up objects that pair a function to run with a function to undo. Dropping down to plain data becomes necessary when you cross a serialization boundary, as with saving or transmitting. There you line up the kinds of operation in a discriminated union and put the function that interprets them somewhere separate. Verdict: a function if you only need to call it, a discriminated union if you serialize it. The latter structure comes out the same as Visitor.
An existing article covers it from another angle. Designing the Use Case Layer names Command explicitly in the PHP and DDD context and lists the benefits of bundling the input into an object such as CreateOrderCommand (type safety, validation, ease of change). What that article targets is passing input across layers, while this section's "a function if you only need to call it" is about carrying an operation around inside one process. The premises differ, so the recommendations are not in conflict.
Bridge
A pattern that separates abstraction from implementation so each can change independently. TypeScript can express the two axes with type parameters or composition, but writing it that way overlaps with what Strategy and Adapter cover. Verdict: it raises little that is specific to TypeScript, so it got no chapter.
Flyweight
A pattern that shares a large number of similar objects to cut cost. Whether it helps is settled by the volume involved and the behavior of the runtime. Verdict: whether it applies is settled by measurement, which falls outside this guide's range of choosing among language features.
Mediator
A pattern that centralizes many-to-many communication in one place. Write the central role with event publishing and subscription and the tools covered in Observer apply directly. Verdict: the intent of centralizing remains, but the implementation tools are the same as Observer's.
Memento
A pattern that saves state so it can be restored later without breaking encapsulation. Plain data can be copied with structuredClone, subject to the same limits as Prototype. Hold what you save as plain objects rather than class instances and those limits never arise in the first place. Verdict: the intent of preserving encapsulation remains, but the tools for restoring end up in the same place as Prototype's.
Interpreter
A pattern that defines a dedicated notation and interprets it. Representing and evaluating a syntax tree is covered in Visitor. Going as far as defining a grammar and parsing turns into designing a language processor, which moves away from design patterns. Verdict: the evaluation half is already covered in Visitor, and the parsing half falls outside this series.
The full list of judgments
Here are the judgments from every chapter. A judgment is this guide's position, not a fact. The grounds, and what would overturn each judgment, are in the chapters themselves.
| Pattern | Verdict | The form to pick by default |
|---|---|---|
| Singleton | conditional | The top level of a module. Take it as an argument if you want isolation in tests or want to swap it |
| Factory Method | language features can replace it | Functions and discriminated unions |
| Abstract Factory | conditional | A function returning the family as a unit, only when the family is decided at runtime |
| Builder | conditional | An object literal. Staged types when there is an ordering constraint or intermediate state |
| Adapter | conditional | Nothing for a name mismatch; a conversion function for a shape mismatch |
| Decorator | conditional | A higher-order function when the target has one method |
| Proxy | conditional | The built-in Proxy when there are many fields or the shape is decided at runtime |
| Composite | conditional | A union type if operations grow, a class if kinds grow |
| Strategy | conditional | A function for one operation, an object when things need bundling |
| Observer | conditional | EventTarget and AbortSignal |
| State | conditional | A discriminated union, with parameter types narrowed to the allowed states |
| Template Method | conditional | Injected hooks when the holes are independent |
| Iterator | language features can replace it | Generators and Symbol.iterator |
| Visitor | language features can replace it | A discriminated union and an exhaustiveness check |
| Chain of Responsibility | language features can replace it | An array of functions plus composition |
| Repository (outside GoF) | conditional | A named interface per aggregate |
Only four came out as "language features can replace it." Everything else landed on conditional. At the same time, no pattern reached "a class implementation is recommended." The conditions under which plain functions stop being enough are written in each chapter, and all of them come with a qualification like these.
- There is state to carry around
- Several operations need bundling
- What grows is kinds rather than operations
- A class hierarchy already exists and adding one method to it is all it takes
- A node carries an invariant on its own state that plain objects cannot uphold
- You want argument types kept strictly separate per kind of event
The patterns have not gone stale. This series' reading is that the number of forms that serve the same purpose grew, turning it into a question of choice.
The steps for deciding whether to use a pattern
Working through the chapters showed that the following order holds up.
- Check whether the problem the pattern set out to solve is actually in your code. The situation GoF assumed and yours can differ. If the problem is absent, you are done right there.
- Check whether a language feature already solves that problem. The iteration protocols, discriminated unions, first-class functions, module boundaries, and
EventTargetall qualify. - Look at what you lose by writing it with language features. Where type checking stops reaching, how much pass-through code you write, and how far a change ripples.
- Write the judgment down, together with what would overturn it. Change the premises and the judgment changes. Without those conditions written down, whoever reads it later takes it as a rule with no reason attached.
What comes up over and over in these steps is not "class or function" but "does anything need bundling," "is there state to carry," and "is it kinds or operations that grow." Many of GoF's patterns are organized as ways of expressing those questions through combinations of classes.
How this relates to existing articles
This series covered design options. Why a given choice is a good one, as a principle, is covered by other articles on this site.
- SOLID principles — the open-closed principle and the dependency inversion principle sit behind several of this series' patterns.
- Classes and interfaces — covers preferring composition over inheritance.
- Conditionals — the basics of discriminated unions and exhaustiveness checks. Several chapters in this series build on it.
- Inheritance and interfaces — covers abstract classes,
implements, and Decorators as a language feature. - Designing the Use Case Layer — as noted in the Command section above, covers Command in the PHP and DDD context.