Factory Method Pattern
Define a creation function whose concrete instantiation logic is deferred to the caller.
The Problemβ
You have a Document class that creates pages. Word documents create WordPage, PDF documents create PdfPage. The OOP approach: an abstract factory method overridden by subclasses.
abstract class Document {
abstract createPage(): Page;
addPage() {
const page = this.createPage();
this.pages.push(page);
}
}
class WordDocument extends Document {
createPage() { return new WordPage(); }
}
class PdfDocument extends Document {
createPage() { return new PdfPage(); }
}
Inheritance to swap a single function call. Every new format means a new subclass.
The Solutionβ
Pass the factory as a parameter. That's dependency injection β no inheritance needed.
type Page = { type: string; content: string };
// The factory is just a parameter β swap it freely
function addPage(pages: Page[], createPage: () => Page): Page[] {
return [...pages, createPage()];
}
const wordFactory = (): Page => ({ type: "word", content: "" });
const pdfFactory = (): Page => ({ type: "pdf", content: "" });
let pages: Page[] = [];
pages = addPage(pages, wordFactory); // [{ type: "word", ... }]
pages = addPage(pages, pdfFactory); // [{ type: "word", ... }, { type: "pdf", ... }]
No inheritance. No abstract classes. Just pass a function.
This solution doesn't use Pithos. That's the point.
In functional TypeScript, passing a factory function as a parameter is the Factory Method pattern. Eidos exports a @deprecated createFactoryMethod() function that exists only to guide you here.
Live Demoβ
Pick a format (Word, PDF, HTML, Markdown) and click Add Page. The addPage function doesn't know which page type it creates β it just calls the injected factory. Swap the format, same function, different output.
- factory.ts
- Usage
/**
* Factory Method β Notification Factory.
*
* sendNotification(createNotif) never changes.
* Only the factory parameter changes β that's the whole pattern.
*/
import type { AppNotification, ChannelKey } from "./types";
const SUBJECTS = ["Your order has shipped", "Weekly digest ready", "Password reset requested", "New comment on your post", "Invoice #4821 available", "Deployment succeeded"];
const EMAILS = ["alice@example.com", "bob@example.com", "team@example.com"];
const PHONES = ["+33 6 12 34 56 78", "+1 555 0123", "+44 7911 123456"];
const SLACK_CHANNELS = ["#general", "#engineering", "#alerts", "#deploys"];
let idx = 0;
function pick<T>(arr: T[]): T { return arr[idx % arr.length]; }
function nextSubject(): string { const s = pick(SUBJECTS); idx++; return s; }
function createEmailNotification(): AppNotification {
const subject = nextSubject();
return { id: crypto.randomUUID(), channel: "email", title: subject, fields: [{ label: "To", value: pick(EMAILS) }, { label: "Subject", value: subject }, { label: "Body", value: "Please find the details attached. Best regards." }], sentAt: Date.now() };
}
function createSmsNotification(): AppNotification {
const subject = nextSubject();
return { id: crypto.randomUUID(), channel: "sms", title: subject, fields: [{ label: "To", value: pick(PHONES) }, { label: "Message", value: subject }], sentAt: Date.now() };
}
function createPushNotification(): AppNotification {
const subject = nextSubject();
return { id: crypto.randomUUID(), channel: "push", title: subject, fields: [{ label: "Title", value: subject }, { label: "Badge", value: `${(idx % 9) + 1}` }, { label: "Sound", value: "default" }], sentAt: Date.now() };
}
function createSlackNotification(): AppNotification {
const subject = nextSubject();
return { id: crypto.randomUUID(), channel: "slack", title: subject, fields: [{ label: "Channel", value: pick(SLACK_CHANNELS) }, { label: "Text", value: subject }, { label: "Bot", value: "pithos-bot" }], sentAt: Date.now() };
}
export const factories: Record<ChannelKey, () => AppNotification> = {
email: createEmailNotification,
sms: createSmsNotification,
push: createPushNotification,
slack: createSlackNotification,
};
/** The consumer that NEVER changes */
export function sendNotification(createNotif: () => AppNotification): AppNotification {
return createNotif();
}
export function resetCounter(): void { idx = 0; }
import { useState, useCallback } from "react";
import { sendNotification, factories, resetCounter } from "@/lib/factory";
import type { AppNotification, ChannelKey } from "@/lib/types";
export function useNotificationDemo() {
const [channel, setChannel] = useState<ChannelKey>("email");
const [notifications, setNotifications] = useState<AppNotification[]>([]);
const [lastSent, setLastSent] = useState<string | null>(null);
const handleSend = useCallback(() => {
const notif = sendNotification(factories[channel]);
setNotifications((prev) => [notif, ...prev]);
setLastSent(notif.id);
setTimeout(() => setLastSent(null), 600);
}, [channel]);
const handleReset = useCallback(() => {
setNotifications([]);
setLastSent(null);
resetCounter();
}, []);
return { channel, setChannel, notifications, lastSent, handleSend, handleReset };
}
APIβ
- factoryMethod
@deprecatedβ use dependency injection instead
Related
- Eidos: Design Patterns Module All 23 GoF patterns reimagined for functional TypeScript
- Why FP over OOP? The philosophy behind Eidos: no classes, no inheritance, just functions and types
- Zygos Result Wrap factory calls in
Resultfor typed error handling