The first sign you want a factory
You want to return different implementations depending on input:
function createStorage(config: { driver: 'memory' | 'redis' | 's3' }): Storage {
if (config.driver === 'memory') return new MemoryStorage();
if (config.driver === 'redis') return new RedisStorage();
return new S3Storage();
}
A constructor that can return three different classes is doing two jobs at once. A factory function is the natural home for that dispatch.
Validation before construction
Constructors that throw are painful to test and awkward to compose. A factory can return a Result:
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
function createUser(input: unknown): Result<User> {
const parsed = UserSchema.safeParse(input);
if (!parsed.success) return { ok: false, error: parsed.error.message };
return { ok: true, value: new User(parsed.data) };
}
Callers handle both outcomes explicitly. No try/catch, no half-constructed objects.
Hiding the construction step entirely
Sometimes the class itself should not be exported. Callers get objects but cannot subclass or inspect internals:
// user.ts
class UserImpl { /* ... */ }
export type User = InstanceType<typeof UserImpl>;
export function createUser(data: UserData): User {
return new UserImpl(data);
}
Consumers import createUser and the User type. They cannot new UserImpl(). This gives you freedom to swap the implementation later — maybe UserImpl becomes a plain object, maybe it becomes a Proxy. Nothing outside the module breaks.
The pattern I use most
For domain objects I rarely use classes at all. A factory returning a plain object with closures is smaller and easier to test:
export function createOrder(items: Item[]) {
const lines = [...items];
return {
addItem: (item: Item) => lines.push(item),
total: () => lines.reduce((sum, l) => sum + l.price, 0),
lines: () => [...lines],
};
}
No this binding headaches. No worrying about .bind() when passing methods as callbacks. The methods close over lines directly.
When classes actually win
Classes are better when:
- You need
instanceofchecks (error hierarchies, especially) - You are interoperating with frameworks that expect classes (NestJS decorators, TypeORM entities)
- You have a large method count and want prototype-based memory savings
For everything else, factory functions are my default. They compose better, test better, and require less mental overhead when reading.
The rule of thumb
If construction is a single, always-succeeding step with no runtime decisions, use a class. The moment you want to validate, branch, or hide the type, reach for a factory.
