15 minlesson

Factory Method in TypeScript

Factory Method in TypeScript

TypeScript offers several ways to implement Factory Method, from classic inheritance to more functional approaches.

Classic Implementation

The traditional OOP approach:

typescript
1// Product interface
2interface ShippingLabel {
3 trackingNumber: string;
4 carrier: string;
5 print(): void;
6}
7
8// Creator with factory method
9abstract class LabelFactory {
10 abstract createLabel(shipment: Shipment): ShippingLabel;
11
12 processShipment(shipment: Shipment): void {
13 const label = this.createLabel(shipment);
14 label.print();
15 }
16}
17
18// Concrete creator
19class FedExLabelFactory extends LabelFactory {
20 createLabel(shipment: Shipment): ShippingLabel {
21 return new FedExLabel(shipment);
22 }
23}

Generic Factory Method

Use generics for type-safe factories:

typescript
1interface Product {
2 id: string;
3}
4
5abstract class Factory<T extends Product> {
6 abstract create(data: unknown): T;
7
8 createMany(dataList: unknown[]): T[] {
9 return dataList.map(data => this.create(data));
10 }
11}
12
13// Type-safe concrete factory
14interface Package extends Product {
15 weight: number;
16}
17
18class PackageFactory extends Factory<Package> {
19 create(data: { id: string; weight: number }): Package {
20 return {
21 id: data.id,
22 weight: data.weight
23 };
24 }
25}
26
27const factory = new PackageFactory();
28const pkg = factory.create({ id: 'PKG001', weight: 10 });
29// pkg is typed as Package

Interface-Based Factory

Use interfaces instead of abstract classes:

typescript
1interface LabelFactory {
2 createLabel(shipment: Shipment): ShippingLabel;
3}
4
5class USPSLabelFactory implements LabelFactory {
6 createLabel(shipment: Shipment): ShippingLabel {
7 return new USPSLabel(shipment);
8 }
9}
10
11// Factory function that works with any implementation
12function processWithFactory(
13 factory: LabelFactory,
14 shipment: Shipment
15): void {
16 const label = factory.createLabel(shipment);
17 label.print();
18}

Functional Factory

TypeScript supports functional programming approaches:

typescript
1// Factory as a function type
2type LabelCreator = (shipment: Shipment) => ShippingLabel;
3
4// Factory functions
5const createUSPSLabel: LabelCreator = (shipment) => new USPSLabel(shipment);
6const createFedExLabel: LabelCreator = (shipment) => new FedExLabel(shipment);
7
8// Higher-order function for processing
9function withLogging(creator: LabelCreator): LabelCreator {
10 return (shipment) => {
11 console.log('Creating label...');
12 const label = creator(shipment);
13 console.log(`Created: ${label.trackingNumber}`);
14 return label;
15 };
16}
17
18const loggedFedEx = withLogging(createFedExLabel);

Factory with Constructor Types

Pass class constructors as factories:

typescript
1interface ShippingLabelConstructor {
2 new (shipment: Shipment): ShippingLabel;
3}
4
5function createLabel(
6 LabelClass: ShippingLabelConstructor,
7 shipment: Shipment
8): ShippingLabel {
9 return new LabelClass(shipment);
10}
11
12// Usage
13const label = createLabel(USPSLabel, shipment);

Factory Map Pattern

Use a Map for dynamic factory lookup:

typescript
1type Carrier = 'usps' | 'fedex' | 'ups';
2
3const labelFactories: Record<Carrier, LabelCreator> = {
4 usps: (s) => new USPSLabel(s),
5 fedex: (s) => new FedExLabel(s),
6 ups: (s) => new UPSLabel(s),
7};
8
9function createLabelForCarrier(
10 carrier: Carrier,
11 shipment: Shipment
12): ShippingLabel {
13 const factory = labelFactories[carrier];
14 return factory(shipment);
15}

Async Factory Method

Handle async creation:

typescript
1abstract class AsyncLabelFactory {
2 abstract createLabel(shipment: Shipment): Promise<ShippingLabel>;
3
4 async processShipment(shipment: Shipment): Promise<void> {
5 const label = await this.createLabel(shipment);
6 label.print();
7 }
8}
9
10class FedExLabelFactory extends AsyncLabelFactory {
11 async createLabel(shipment: Shipment): Promise<ShippingLabel> {
12 // Fetch tracking number from FedEx API
13 const tracking = await this.fetchTrackingNumber(shipment);
14 return new FedExLabel(shipment, tracking);
15 }
16
17 private async fetchTrackingNumber(shipment: Shipment): Promise<string> {
18 // API call simulation
19 return `FEDEX${Date.now()}`;
20 }
21}

Factory with Validation

Add validation to the creation process:

typescript
1abstract class ValidatingLabelFactory {
2 abstract createLabel(shipment: Shipment): ShippingLabel;
3
4 protected abstract validateShipment(shipment: Shipment): void;
5
6 createValidatedLabel(shipment: Shipment): ShippingLabel {
7 this.validateShipment(shipment);
8 return this.createLabel(shipment);
9 }
10}
11
12class InternationalLabelFactory extends ValidatingLabelFactory {
13 createLabel(shipment: Shipment): ShippingLabel {
14 return new InternationalLabel(shipment);
15 }
16
17 protected validateShipment(shipment: Shipment): void {
18 if (!shipment.customsDeclaration) {
19 throw new Error('International shipments require customs declaration');
20 }
21 if (!shipment.destination.country) {
22 throw new Error('Destination country is required');
23 }
24 }
25}

Testing with Factory Method

Factories make testing easier:

typescript
1// Test double
2class MockLabelFactory extends LabelFactory {
3 public lastCreatedLabel: ShippingLabel | null = null;
4
5 createLabel(shipment: Shipment): ShippingLabel {
6 const mockLabel: ShippingLabel = {
7 trackingNumber: 'MOCK123',
8 carrier: 'mock',
9 print: vi.fn(),
10 };
11 this.lastCreatedLabel = mockLabel;
12 return mockLabel;
13 }
14}
15
16// In tests
17describe('ShipmentProcessor', () => {
18 it('should create and print label', () => {
19 const mockFactory = new MockLabelFactory();
20 const processor = new ShipmentProcessor(mockFactory);
21
22 processor.process(testShipment);
23
24 expect(mockFactory.lastCreatedLabel?.print).toHaveBeenCalled();
25 });
26});

Best Practices

  1. Use interfaces for products - Maximum flexibility
  2. Consider functional approach - Simpler for small cases
  3. Combine with Registry - For dynamic factory lookup
  4. Add validation in factory - Fail fast with clear errors
  5. Make factories testable - Easy to mock in tests

Summary

ApproachUse Case
Abstract classNeed shared behavior in creator
InterfaceMaximum flexibility
FunctionalSimple cases, composition
GenericType-safe reusable factories
AsyncNetwork/IO during creation

Next: Build a shipping label factory workshop!