Typing Factory Pattern Implementations in TypeScript
Introduction
Factories are a fundamental design pattern for creating objects without coupling your code to concrete implementations. In TypeScript projects factories appear everywhere: dependency injection containers, plugin systems, serializers, test fixtures, and libraries exposing extensible APIs. For intermediate developers, the challenge is not only to implement factories that work at runtime, but to create types that are safe, ergonomic, and maintainable as code evolves.
In this tutorial you will learn how to type factory pattern implementations in TypeScript end to end. We cover basic typed factory functions, generics, class-based factories, abstract factories, factories that accept variadic parameters, factories that bind methods or modify this, and factories that produce iterables or async producers. You will see concrete code examples, step-by-step typing strategies, and techniques to keep runtime flexibility while preserving strong static guarantees.
By the end you will be able to design factory APIs that provide precise return types, preserve constructor signatures, work with subclassing, and interoperate with third party libraries while minimizing runtime checks. Expect plenty of examples and practical rules you can apply right away in real projects.
Background & Context
The factory pattern separates object creation from object use. A factory encapsulates construction logic and can return different implementations based on configuration or environment. When you add TypeScript to the mix, naive factory typings often fall short: you may lose inference of specific properties, fail to capture constructor parameter types, or create wealthy union types that are hard to use.
Typing factories well improves developer experience, prevents runtime errors, and allows refactors to remain safe. This tutorial focuses on patterns that balance expressiveness and simplicity. We also link to related TypeScript topics when deeper knowledge is helpful, such as typing class constructors, abstract class members, and typing functions that modify this.
Key Takeaways
- Understand how to type basic factory functions and protect invariants
- Use generics and constructor signatures to preserve parameter and return types
- Implement abstract factory typings for extensible systems
- Handle variadic constructor parameters using tuple types and rest inference
- Bind methods safely using ThisParameterType and OmitThisParameter when factories wrap methods
- Type factory-produced iterables and async factories safely
- Integrate runtime guards and overloads for third party or dynamic APIs
Prerequisites & Setup
To follow the examples you need TypeScript 4.2 or newer. Use a modern editor like VS Code with TypeScript support to see inference and errors. Initialize a small project and install types if you plan to run compiled code, but examples are type-level first and runnable via tsc with strict mode enabled. Recommended tsconfig options include "strict", "noImplicitAny", and "strictFunctionTypes".
If you need a refresher on typing class constructors in TypeScript, consider the linked guide on typing class constructors which explains constructor signatures and newable types in detail.
Main Tutorial Sections
1. Basic factory function with explicit return type
Start with a plain factory that returns a shaped object. Explicit return types make APIs stable but can lose inference when the factory adapts based on inputs.
Example:
type Logger = { log: (msg: string) => void }
function createConsoleLogger(): Logger {
return { log: msg => console.log(msg) }
}
const lg = createConsoleLogger()
lg.log('hello')Actionable steps
- Declare a clear interface for the product type
- Return that interface from the factory rather than object literals directly, to prevent leaking implementation details
When the factory has branches, prefer union return types or generic constraints to preserve intent.
2. Generic factories that preserve specific implementations
Generic factories let callers specify the desired product type, enabling better inference and narrowing.
Example:
interface ServiceA { kind: 'a'; doA: () => void }
interface ServiceB { kind: 'b'; doB: () => void }
function createService<T extends ServiceA | ServiceB>(type: T['kind']): T {
if (type === 'a') {
return { kind: 'a', doA: () => {} } as T
}
return { kind: 'b', doB: () => {} } as T
}
const a = createService<ServiceA>('a')
a.doA()Actionable steps
- Add a type parameter constrained to possible products
- When necessary, use type assertions at the point of return, but keep them minimal
- Consider function overloads if you want to infer without explicit type arguments
For more on typing complex third party APIs and runtime guards when you accept dynamic input, see typing third party libraries with complex APIs.
3. Factory based on constructors and preserving parameter types
When factories wrap classes, capturing constructor argument types and instance types is important. Use the newable signature pattern.
Example:
class Foo { constructor(public name: string, public count = 0) {} }
type Constructor<T> = new (...args: any[]) => T
function factoryFromCtor<C extends Constructor<InstanceType<C>>, Ctor extends Constructor<any>>(Ctor: Ctor, ...args: ConstructorParameters<Ctor>): InstanceType<Ctor> {
return new Ctor(...args)
}
const foo = factoryFromCtor(Foo, 'alice', 3)
foo.nameActionable steps
- Use ConstructorParameters and InstanceType utilities to preserve signatures
- Keep generic constraints narrow to improve inference
See the deep dive on typing class constructors for patterns and edge cases.
4. Abstract factory pattern with interfaces and abstract classes
Abstract factory pattern encodes families of related products. Types help enforce compatibility between factory implementations.
Example:
interface Button { render: () => void }
interface Checkbox { toggle: () => void }
interface UIFactory {
createButton(): Button
createCheckbox(): Checkbox
}
class MacFactory implements UIFactory {
createButton() { return { render: () => console.log('mac button') } }
createCheckbox() { return { toggle: () => console.log('mac toggle') } }
}
function buildUI(factory: UIFactory) {
const b = factory.createButton()
b.render()
}Actionable steps
- Use interfaces to describe the factory contract
- Switch concrete factory by dependency injection to allow test doubles
If your factories involve abstract members or subclasses, the article on typing abstract class members provides patterns for typing abstract methods and fields.
5. Class-based factories, static members and subclassing
Class factories often expose static creation helpers. Typing them so subclasses inherit correct signatures requires care.
Example:
class Model {
static create<T extends typeof Model>(this: T, attrs: Partial<InstanceType<T>>) {
return new this(attrs) as InstanceType<T>
}
constructor(public data: any) {}
}
class User extends Model {
constructor(public data: { name: string }) { super(data) }
}
const u = User.create({ name: 'sam' })Actionable steps
- Add a this type on static methods to preserve subclass behavior
- Use InstanceType to map constructors to instance types
For more on static members and strategies, check typing static class members.
6. Factories with variadic parameters and tuple inference
Some factories forward parameters to constructors or functions with varying arities. Variadic tuple types enable precise typing.
Example:
function makeFactory<C extends new (...args: any[]) => any>(Ctor: C) {
return (...args: ConstructorParameters<C>): InstanceType<C> => {
return new Ctor(...args)
}
}
class Item { constructor(public a: string, public b: number) {} }
const itemFactory = makeFactory(Item)
const it = itemFactory('x', 2)Actionable steps
- Use ConstructorParameters to capture argument tuples
- Type the factory rest parameters as the same tuple to preserve arity and element types
If you need a refresher on typing function parameters as tuples and variadic signatures, see typing function parameters as tuples in TypeScript and typing functions that accept a variable number of arguments.
7. Factories that bind methods and modify this
When factories wrap objects and rebind methods, this typing matters. Use ThisParameterType and OmitThisParameter to properly type wrapped methods.
Example:
function bindAll<T extends Record<string, any>>(obj: T) {
const out: Partial<T> = {}
for (const k in obj) {
const v = obj[k]
if (typeof v === 'function') {
out[k] = (v as Function).bind(obj)
} else {
out[k] = v
}
}
return out as T
}
const svc = {
x(this: { v: number }) { return this.v }
}
const bound = bindAll(svc)Actionable steps
- Use utility types like ThisParameterType and OmitThisParameter to strip or reattach this correctly
- Avoid assertions by preserving call signatures via mapped types where possible
See typing functions that modify this for patterns to safely change this in factories that wrap methods.
8. Factories producing iterables and async iterables
Factories can return generators or async producers. Typing these correctly improves consumer ergonomics and integrates with for..of and for await..of loops.
Example:
function rangeFactory(n: number) {
return function* () {
for (let i = 0; i < n; i++) yield i
}
}
const numbers = rangeFactory(3)()
for (const v of numbers) console.log(v)If your factory returns async iterators, type the return as AsyncIterable
Actionable steps
- Use Iterable
or AsyncIterable as return types - Prefer generator function signatures for ergonomic consumption
9. Typing factories that integrate with third party libraries
When a factory acts as an adapter for an untyped or loosely typed external API, combine runtime checks with narrow type wrappers.
Example:
type External = any
function adaptExternalFactory(ex: External) {
if (typeof ex.create !== 'function') throw new Error('not supported')
return function create() {
return ex.create()
}
}Actionable steps
- Add runtime guards and narrow types before exposing typed interfaces
- Use branded types or opaque wrappers to avoid leaking unsound values
For a comprehensive approach to typing third party libraries and handling complex runtime shapes, read typing third party libraries with complex APIs.
Advanced Techniques
Once comfortable with the previous patterns, consider advanced type strategies. Conditional types let you map factory inputs to different outputs. Mapped types combined with generics can produce factory maps where each key has a matching create signature. Use infer inside conditional types to extract constructor or function parameter tuples and return types automatically.
Example pattern:
type FactoryFrom<C> = C extends new (...a: infer A) => infer R ? (...a: A) => R : never
Use type-level utilities to create registries with compile time safety, for example mapping a string key union to constructors and exposing a single create function that narrows by key. Avoid overcomplicating types for trivial needs; prefer simpler generics that your team will understand.
Performance tip: complex conditional and recursive types increase compiler work. Keep type-level recursion shallow and evaluate whether some invariants can be checked at runtime instead of via types to improve incremental compilation speed.
Best Practices & Common Pitfalls
Dos
- Do preserve constructor parameter types using ConstructorParameters when wrapping classes
- Do prefer explicit interfaces for public factory return types to avoid leaking implementation details
- Do add runtime guards when accepting untrusted external inputs
- Do use ThisParameterType and OmitThisParameter when rebinding methods so callers keep the right this
Dont's
- Don’t overuse type assertions to silence the compiler; instead refactor types to represent intent
- Don’t rely on any in factory internals without narrowing before exposing results
- Don’t expose internal class prototypes or private members through factory outputs
Common pitfalls
- Losing inference when a generic type parameter is required from callers. Consider overloads or key-based factory signatures so callers need not pass explicit generics.
- Mixup of static and instance types. Use this typing conventions on static factory methods to preserve subclass behavior. See typing static class members for guidance.
- Incorrect this handling resulting in runtime errors when methods are called detached from their instance. Use bound wrappers or utilities covered earlier.
If you frequently use getters and setters in products, ensure the factory returns the proper property descriptors or typed accessors. See typing getters and setters in classes and objects for patterns.
Real-World Applications
Factory patterns are especially useful in these scenarios:
- Dependency injection containers where services are registered by token and resolved lazily
- Plugin systems that load modules and instantiate plugin classes or factories at runtime
- Serialization and deserialization layers where factories produce typed domain objects from raw data
- Test fixtures and builders for unit tests, enabling creation of deterministic test objects with correct types
Example: a plugin registry maps plugin ids to constructors. A typed registry keeps the mapping safe and gives consumers precise types for plugin instances. Use constructor-preserving factories to allow plugins with varied constructor shapes to be created safely.
Conclusion & Next Steps
Typing factory patterns well unlocks safer, more maintainable APIs. Start by modeling the product, preserve constructor and parameter shapes with utility types, and add runtime guards when inputs are dynamic. Next, explore related TypeScript features covered in the linked articles to deepen your knowledge: constructors, abstract members, and function this handling.
Recommended next reads include our guides on typing class constructors, typing functions that modify this, and typing third party libraries with complex APIs.
Enhanced FAQ
Q1: When should I use a generic factory vs union return types?
A1: Use a generic factory when the caller can or should specify the expected concrete product and you want to preserve precise types on the returned value. Use union return types when the factory decides the product based on internal logic or runtime configuration and callers will inspect discriminants to narrow. Generics offer better inference for consumers who know the expected type up front, while unions capture dynamic behavior.
Q2: How do I preserve constructor parameter types when wrapping classes?
A2: Use the built in utility types ConstructorParameters
Q3: Is it safe to use type assertions inside factory implementations?
A3: Type assertions can be used sparingly when you verify invariants at runtime but the compiler cannot prove them. Prefer to narrow types via guards and design types to reflect runtime invariants. Excessive assertions mask problems and reduce the value of static types.
Q4: How do I type factories that return functions which use this?
A4: If the produced function relies on this, prefer binding the instance at creation time or use ThisParameterType and OmitThisParameter to adjust call signatures. Alternatively, convert methods to arrow functions bound to the instance during construction so callers can call them safely detached from the object.
Q5: What pattern should I use for a registry of factories keyed by strings?
A5: Define a mapping interface where keys map to constructor types or factory function types. For example type Registry = { [K in keyof Services]: new (...a: any[]) => Services[K] }. Then implement a typed create function like function create
Q6: Can factories return unions of types without hurting inference?
A6: They can, but consumers must perform narrowing using discriminant properties or type guards. If the union is large or not easily discriminated, prefer separate factory methods or overloads so callers get precise types without manual narrowing.
Q7: How do I handle asynchronous construction in factories?
A7: Return Promise
Q8: My factory needs to create instances of classes with private or protected members. How to type that?
A8: Factories typically return public interfaces. Avoid exposing private and protected internals in the returned types. If you must return concrete instances with private fields, the TypeScript type system will only check accessible members. For design clarity, return a public interface that only exposes supported operations. For patterns involving subclass internals, see typing private and protected class members to learn how to model visibility and design APIs that respect encapsulation.
Q9: How should factories interact with getters and setters in product classes?
A9: If your factory constructs instances that rely on getters and setters, ensure the returned type reflects those accessors. Prefer returning the instance type to preserve accessors. If you expose plain object literals, define property signatures with getter semantics where needed. The guide on typing getters and setters in classes and objects has concrete examples.
Q10: How to debug complex type errors that arise when typing factories?
A10: Break types into smaller named aliases, add temporary assertions to isolate the failing point, and use type queries like ReturnType and Parameters to inspect inferred types. Simplify conditional types and test with minimal examples in the TS playground. If compile times spike, consider simplifying recursive types or moving some checks to runtime validators.
If you want hands on exercises, try refactoring an existing registry into a typed factory map using ConstructorParameters and InstanceType and then add overloads so consumers do not need explicit generics. For more foundational reading on related topics, check the linked articles sprinkled through this post, including detailed guides on constructors, static members, abstract members, and modifying this.
