| name | implementation-functional-patterns |
| description | TypeScript's functional answers to the 22 Gang of Four classes — factory functions (Factory Method, Abstract Factory, Prototype, Memento), module-scope singletons, fluent immutable builders, wrapper functions (Adapter, Facade), native Proxy, WeakMap caches (Flyweight), discriminated unions with exhaustive match (State, Visitor, Composite), event emitters and signals (Mediator, Observer), pipelines and composition (CoR, Decorator), stream methods (Iterator), closures-as-commands, higher-order strategies, lambda placement. Use when reviewing TypeScript that has a class-shaped problem the GoF catalog solves with a hierarchy but where idiomatic TS reaches for a function, a tagged union, or a data structure. Each rule names the GoF pattern(s) it replaces and when the class form still wins. Trigger on "factory class", "singleton getInstance", "state machine class", "observer pattern", "AST visitor", "where do I put this lambda". Sibling to implementation-design-patterns. |
TypeScript Functional Patterns
Implementation reference for the functional shapes that supersede the Gang of Four catalog in idiomatic TypeScript. Sibling to implementation-design-patterns: read that one when the answer is a class, this one when the answer is a function, a tagged union, or a small data structure.
TypeScript has first-class functions, discriminated unions, structural typing, and zero ceremony around closures. That means the 22 GoF patterns — written in the catalog as class hierarchies because the source material targets Java/C# — collapse to far fewer functional shapes in real TS code. Several patterns share a functional answer: tagged unions cover State, Visitor, and Composite; factory functions cover Factory Method, Abstract Factory, Prototype, and Memento; event emitters cover Mediator and Observer; wrapper functions cover Adapter and Facade. This skill names those collapses, the placement rules they imply, and the performance trade-offs.
When to Apply
- Refactoring a Factory class hierarchy, an
AbstractFactory returning families of products, or a class with clone() / Memento save-restore
- Refactoring a Singleton class with
private constructor + static getInstance
- Refactoring a mutable Builder class for a configuration object with many optional fields
- Refactoring a Strategy / Template Method / Bridge class hierarchy where variation is a single method
- Replacing a Chain of Responsibility class chain or Decorator wrapper-class stack
- Replacing a custom Iterator class with stream methods, generators, or lazy iterator helpers
- Replacing a Command class with a closure stored in a queue (when undo/serialization is not required)
- Replacing an Adapter / Facade class that exists only to forward calls or hide subsystem orchestration
- Replacing a Proxy class with native JS
Proxy (for transparent interception) or an HOF wrapper (for selective wrapping)
- Replacing a Flyweight factory class with a
Map/WeakMap cache + factory function
- Replacing a State / Visitor / Composite class hierarchy with a discriminated union + exhaustive
match function
- Replacing a Mediator / Observer class with an event emitter or reactive signal
- Reviewing TSX where lambdas appear inline in JSX, hook deps, or
memo'd child props — placement controls identity
- Recognizing imperative
for loops with mutated accumulators that would read more honestly as reduce, Object.groupBy, or flatMap
Rule Categories
| # | Category | Impact | Rules | Theme |
|---|
| 1 | Creational alternatives (create) | HIGH | 3 | Factory functions, module-scope singletons, fluent immutable builders |
| 2 | Higher-order functions (hof) | HIGH | 1 | Pass a function instead of a class |
| 3 | Pipelines & composition (pipe) | HIGH | 2 | Compose small functions: pipe (data flow), compose (wrapper layering) |
| 4 | Stream methods (stream) | HIGH | 4 | map/filter/flatMap/reduce, lazy iteration, single-pass chains |
| 5 | Wrappers (wrap) | HIGH | 2 | Wrapper functions for Adapter/Facade; native Proxy or HOF for Proxy |
| 6 | Caching & sharing (cache) | HIGH | 1 | Map/WeakMap + factory function over Flyweight class |
| 7 | Pattern matching (match) | HIGH | 1 | Discriminated unions + exhaustive match for State/Visitor/Composite |
| 8 | Signals & event emitters (signal) | HIGH | 1 | Event emitter / signal for Mediator/Observer |
| 9 | Placement & identity (place) | HIGH | 1 | Where the lambda lives controls behavior |
| 10 | Closures as data (closure) | MEDIUM | 1 | Functions that carry their state |
17 rules across 10 categories, covering all 22 GoF patterns (some patterns share a functional answer — see the GoF → Functional Map below).
GoF → Functional Map
The full mapping from each Gang of Four pattern to its functional answer in idiomatic TS. Read this table to find the rule for a specific pattern; read the categories above to find rules by functional technique.
How to Use
- Find the pattern. If you know which GoF pattern you'd reach for, look it up in the GoF → Functional Map above and read the linked rule. If you only know the symptom (loop with accumulator, class returning class, three setters), the Quick Reference below groups rules by functional technique.
- Read "When NOT to apply". Every rule lists the narrow conditions where the class form still wins. The skill is a complement to the parent skill, not a repudiation — keep the class when serialization, runtime introspection, typed inter-pattern relations, cross-cutting state, framework integration, or lifecycle ownership demands it.
- Check identity assumptions in TSX. If the code lives in a TSX file or runs inside a hook, also read the
place-* rules — placement decides whether memo, useEffect, and React Compiler can do their jobs.
- Mind the performance. Every rule has a
### Performance trade-offs section quantifying time, memory, and allocation costs. Most functional forms are performance-equivalent to the class form; a few (chained streams, flatMap) have real constant-factor costs that matter in hot paths.
Quick Reference
1. Creational alternatives
create-factory-function-over-factory-classes — Function returning a tagged object instead of a Factory class hierarchy. Covers Factory Method, Abstract Factory, Prototype, and Memento. "I'd write new FooFactory().create() — but a function returning the tagged object is shorter and tree-shakes." — HIGH
create-module-scope-over-singleton — export const x = … or lazy ??= instead of class X { private static instance; getInstance() }. ES modules ARE singletons; the class form is anti-idiom in TS. "I need exactly one of these — config, DB client, logger." — HIGH
create-fluent-immutable-builder — Object literal + Partial<T> for simple cases; fluent immutable (each method returns a new builder) for type-state-tracked DSLs. "My constructor has 10 parameters / my Builder class has 8 setters." — HIGH
2. Higher-order functions
hof-lambda-as-strategy — Pass a comparator/predicate/transformer lambda instead of defining a Strategy class. Also covers Template Method (HOF with step callback) and Bridge (HOF parametrized by implementation). "My Strategy interface has one method." — HIGH
3. Pipelines & composition
pipe-pipeline-over-chain-of-responsibility — pipe(validate, authorize, parse)(req) or an array fold of handlers, instead of a linked list of Handler classes. "My CoR chain handlers each do one transform and pass the result along." — HIGH
pipe-compose-over-decorator — compose(withLogging, withCache, withAuth)(handler) — each wrapper is (handler) => handler, not a Decorator class. Right-to-left order reads top-down like the class stack. "I want to add logging + caching + auth around this handler." — HIGH
4. Stream methods
stream-flatmap-over-nested-loops — .flatMap for one-to-many transforms instead of for + push or map().reduce(concat). "For each user, expand to all their orders, then collect." — HIGH
stream-reduce-over-imperative-accumulation — reduce / Object.groupBy / Map.groupBy instead of let acc = …; for (…) acc[…] = …. The most common functional pattern in real TS: sums, counts, indexes, histograms. "I'm building up a total / index / grouped map in a loop." — HIGH
stream-lazy-iteration-for-large-or-infinite — Generators or TC39 Iterator helpers (Iterator.from(arr).filter(p).take(10).toArray()) when only a prefix of results is needed. O(matched-needed) instead of O(n). "First N matches from a huge or unbounded source." — HIGH
stream-prefer-single-pass-over-chained-passes — Collapse .filter().map().filter() into one reduce or for-of when the input is large or the chain is hot. Three passes → one; halves peak memory; 2–5× faster in measured hot paths. "This chain runs on every request / render over thousands of items." — HIGH
5. Wrappers
wrap-function-over-adapter-and-facade — Wrapper function translating one interface to another (Adapter) or hiding subsystem orchestration (Facade). "I'd write class XAdapter implements Y whose every method is one-line forwarding." — HIGH
wrap-proxy-native-or-hof — Native Proxy for transparent dynamic-key interception, HOF wrapper for selective method-level wrapping. "I want lazy loading / access control / interception without a Proxy class." — HIGH
6. Caching & sharing
cache-weakmap-over-flyweight — WeakMap/Map cache + factory function instead of Flyweight factory class. WeakMap auto-cleans when keys go out of scope. "I'm allocating millions of similar objects with shared state." — HIGH
7. Pattern matching
match-tagged-union-over-state-visitor-composite — Discriminated union + switch on tag + assertNever — covers State (class per state), Visitor (double dispatch), and Composite (Leaf/Branch). Probably the single highest-payoff functional pattern in TS. "My class has a giant switch on kind / I have N state classes / I'd write a Visitor over my AST." — HIGH
8. Signals & event emitters
signal-event-emitter-over-mediator-and-observer — Event emitter (mitt, EventEmitter) or reactive signal (Solid, Preact signals, Zustand) instead of Subject/Observer classes or central Mediator class. "Many objects need to react when one changes / form fields need to coordinate / cross-component events." — HIGH
9. Placement & identity
place-module-scope-pure-transformers — Put pure transformer lambdas at module scope (stable identity, reusable, tree-shakable). Nest them inside a function only when they capture something. "This (s) => s.toLowerCase() doesn't need to be in the component body." — HIGH
10. Closures as data
closure-as-command — Store a () => void closure in the queue/history/callback list instead of a Command class with execute(). "I need a queue of deferred operations and never need to inspect or serialize them." — MEDIUM
How to Choose: Class vs Function
The class form (see implementation-design-patterns) earns its overhead when at least one of these is true:
- Serialization — Commands or Mementos that must survive a process restart or cross a wire
- Runtime registry / introspection — the system enumerates known strategies, displays them in a picker, or attaches metadata
- Typed inter-pattern relations — Visitor over an AST where node types reference each other, State machine where states reference each other, Mediator with typed roles
- Cross-cutting state — the "variation" carries its own configuration, lifecycle, or invariants beyond the single call
- Stable identity for
instanceof — exhaustive matching on a finite set of named classes (rare; discriminated unions usually win)
- Lifecycle ownership — the object owns a resource (connection, file handle, disposable) and
using / Symbol.dispose integration matters
- Framework integration — ORMs, DI containers, decorator-based libraries, RxJS class-based services expect classes
Otherwise, default to the function (or tagged union, or data structure). The class wraps the value in ceremony that earns nothing.
References
- MDN —
Array.prototype
- TC39 — Iterator Helpers proposal
- Mostly Adequate Guide to Functional Programming (Brian Lonsdorf)
- TC39 — Pipeline Operator proposal
- MDN — Closures
- TS Handbook — Discriminated Unions
- MDN —
Proxy
- MDN —
WeakMap
- MDN —
structuredClone