Creational Design Patterns: Factories, Builders & Const Construction
Object creation in enterprise TypeScript must balance complex runtime construction logic with compile-time type safety. This article covers Factory Method, Abstract Factory, ESM vs. class Singletons, and a type-safe Builder pattern using TypeScript 5.0 `<const T>` generics and phantom types to prevent incomplete construction at compile time.
TypeScript Low-Level Design & Object-Oriented Architecture
Creational Design Patterns: Factories, Builders & Const Construction
In TypeScript 5+, OOP is an architectural contract, not an inheritance tree: enforce domain invariants at compile time, encapsulate mutation strictly within aggregates, and invert dependencies so high-level business policy never couples to execution details. The creational patterns in this article are the mechanism for enforcing that contract at the moment of object birth — ensuring that no partially-constructed, invariant-violating object can exist in the system.
Classical GoF creational patterns were designed for nominal, statically-typed OOP languages. TypeScript 5+ offers something those languages could not: phantom types and <const T> generics that make illegal construction a compile-time impossibility rather than a runtime exception.
1. The Anti-Pattern Graveyard: The Telescoping Constructor
Here is the object construction pattern that breaks silently under parameter re-ordering:
Swapping shippingAddressId and billingAddressId produces a runtime bug. Both are string. The type system cannot distinguish them without branding (Part 2), and the call site provides no visual anchors. Adding an optional parameter requires updating every call site.
2. Factory Method: Encapsulating Construction Branching
2.1 When to Use a Factory
Use a Factory Method when:
- Object creation involves runtime branching based on environment, configuration, or user input
- The caller should not know which concrete class it receives — only the interface
- Future variants may be added without changing the call site
2.2 Abstract Factory: Coordinating Families of Products
An Abstract Factory produces multiple related objects that must be used together consistently. In the fintech domain, this is the SandboxLedgerFactory vs ProductionLedgerFactory pattern — every object created by a sandbox factory is a sandbox object:
3. Singletons: ESM Module vs. Class Static
3.1 Why Class Singletons Are Problematic
3.2 ESM Module Singleton
In tests, mock the module: jest.mock('./config', () => ({ configService: { get: jest.fn() } })). Each test file gets a fresh module scope — no shared state between test files.
4. Type-Safe Builder with Phantom Types
The Builder pattern solves telescoping constructors by assembling objects step-by-step via method chaining. TypeScript 5+ takes this further: phantom type parameters track which fields have been set at the type level, making incomplete construction a compile-time error.
4.1 The Phantom Type State Machine
The phantom type approach moves the "required field" check from runtime (if (!this.items) throw) to compile time. No test can catch a missing .withItems() call faster than the TypeScript compiler.
5. TypeScript 5.0 <const T> Generic Parameters
Before TS 5.0, builders and factories that accepted configuration objects had to ask callers to annotate with as const to preserve literal types. TS 5.0 eliminates this requirement:
This pattern is especially useful in strategy registries (Part 6), plugin systems, and route configuration builders where preserving literal types enables downstream type inference.
6. Interview Application: Factory vs Builder Decision
In an LLD interview with a 60-minute limit, a common mistake is spending 15 minutes implementing a full Builder when a simple factory function would have been sufficient. Here is the heuristic:
| Construction Need | Use |
|---|---|
| Runtime type selection (env/config-based) | Factory function / Factory Method |
| Multiple coordinated objects | Abstract Factory |
| Single instance per process | ESM module export |
| Complex object with many optional fields | Builder Pattern |
| Builder where field combinations must be validated at compile time | Builder + Phantom Types |
| Object cloning with structural sharing | Spread / Object.assign |
Summary
| Pattern | Use When | TS 5+ Feature |
|---|---|---|
| Factory Function | Runtime type selection, simple branching | Type-narrowing switch with literal union return |
| Abstract Factory | Coordinated families of related objects | Interface-typed factory methods |
| ESM Singleton | One shared stateless instance per module graph | Module-level export const |
| Builder + Phantom Types | Complex objects with required field validation | this: Builder<Set, Set, Set> constraint |
<const T> Generics |
Preserve literal types without caller as const |
function f<const T>() |
| Prototype Cloning | Cheap copies of complex objects | Spread / structural sharing |
What's Next
In Part 9, we move to structural patterns — the connective tissue between incompatible interfaces, resource-heavy objects, and hierarchical domain trees. Part 9: Structural Design Patterns: Adapters, Proxies, Facades & Trees covers the Adapter as an anti-corruption layer against vendor SDK churn, native ECMAScript
Proxyfor transparent interception, and the Composite pattern for recursive fee calculation trees.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.