Factory Pattern in TypeScript
Older Article
This article was published 8 years ago. Some information may be outdated or no longer applicable.
Design patterns help us write better software. They’re reusable solutions to problems that keep showing up, and they’ve been around long enough to prove their worth.
GoF
Back in 1994, Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides published a book covering 23 design patterns: Design Patterns: Elements of Reusable Object-Oriented Software. You might know it (or its authors) as the Gang of Four (GoF).
The patterns they laid out still hold up. Every one of them applies to modern applications.
Design Pattern types
Design patterns fall into these categories based on what they aim to achieve:
- Creational: deal with object creation
- Structural: deal with relationships between objects (or other entities)
- Behavioural: deal with communication patterns between objects
- Concurrency: deal with multi-threaded paradigms
Factory Pattern
The factory pattern is a creational design pattern. It provides a generic interface to create objects. It works with a creator that produces products. When you use the factory pattern, you don’t need the new keyword because instantiation happens inside the factory itself.
When to use the factory pattern?
The factory pattern fits situations where the object creation process is complex, or where multiple objects share the same properties.
TypeScript implementation
The factory pattern could be implemented in any language, including vanilla JavaScript. But I thought it’d be interesting to see how a TypeScript version would look.
Should you want the JavaScript version, just transpile the TypeScript code to JavaScript, either to ES2015 or to ES5.
Factory example
Let’s start with a simple factory pattern example. We’ve got a factory that creates superheroes:
// herofactory.ts
export class HeroFactory {
private name: string;
private health: number;
constructor(name: string, health: number) {
this.name = name;
this.health = health;
}
introduce() {
console.log(`Hello, I am ${this.name}. Pleasure to meet you.`);
}
}
export function createHero(name: string, health: number) {
return new HeroFactory(name, health);
}
Using this class is dead simple:
// app.ts
import * as heroFactory from './herofactory';
const spiderman = heroFactory.createHero('Peter', 100);
spiderman.introduce(); // Hello, I am Peter. Pleasure to meet you.
const superman = heroFactory.createHero('Clark', 100);
superman.introduce(); // Hello, I am Clark. Pleasure to meet you.
Let’s take this further.
Say we’re building a game where we can create superheroes. A superhero can be either a good guy (a Hero) or a bad guy (a Villain). These two are represented as different classes:
// hero.ts
export class Hero {
private name: string;
private health: number;
private maxHealth: number = 100;
constructor(name: string, health: number = 100) {
this.name = name;
if (health < this.maxHealth) {
this.health = health;
} else {
this.health = this.maxHealth;
}
}
attacked(attackValue) {
if (attackValue > this.health) {
console.log(`${this.name} is no more.`);
} else {
this.health -= attackValue;
console.log(`${this.name} attacked: ${attackValue} -- ${this.health}`);
}
}
heal(healValue) {
if (this.health + healValue > this.maxHealth) {
this.health = this.maxHealth;
console.log(`${this.name} has max health of ${this.maxHealth}`);
} else {
this.health += healValue;
console.log(`${this.name} healed to ${this.health}`);
}
}
}
// villain.ts
export class Villain {
private name: string;
private health: number;
private maxHealth: number = 100;
constructor(name: string, health: number = 100) {
this.name = name;
if (health < this.maxHealth) {
this.health = health;
} else {
this.health = this.maxHealth;
}
}
rampage() {
if (this.health <= 10) {
this.health = this.maxHealth * 0.9;
console.log(`${this.name} restored health to ${this.health}`);
} else {
console.log(`${this.name} is not weak enough`);
}
}
attacked(attackValue) {
if (attackValue > this.health) {
console.log(`${this.name} is no more.`);
} else {
this.health -= attackValue;
console.log(`${this.name} attacked: ${attackValue} -- ${this.health}`);
}
}
heal(healValue) {
if (this.health + healValue > this.maxHealth) {
this.health = this.maxHealth;
console.log(`${this.name} has max health of ${this.maxHealth}`);
} else {
this.health += healValue;
console.log(`${this.name} healed to ${this.health}`);
}
}
}
Now we create a SuperHeroFactory:
// superherofactory.ts
import { Hero } from './hero';
import { Villain } from './villain';
export class SuperHeroFactory {
createSuperHero(type: Object);
createSuperHero(type: 'hero'): Hero;
createSuperHero(type: 'villain'): Villain;
public createSuperHero(heroOptions): Hero | Villain {
if (heroOptions.type === 'hero') {
const hero = new Hero(heroOptions.name, heroOptions.health);
return hero;
} else if (heroOptions.type === 'villain') {
const villain = new Villain(heroOptions.name, heroOptions.health);
return villain;
} else {
throw new Error('Select either a Hero or a Villain');
}
}
}
Notice the specialised signatures above. If a hero is created we return the Hero class, if a villain is created we return the Villain class. We need these to ensure the correct methods are available. Leaving them out would mean the rampage() method becomes inaccessible: Property 'rampage' does not exist on type 'Hero | Villain'. Property 'rampage' does not exist on type 'Hero'. TypeScript would assume joker is of type Hero, which is wrong.
Finally, we implement the factory:
// app.ts
import { SuperHeroFactory } from './superherofactory';
const superheroFactory = new SuperHeroFactory();
const batman = superheroFactory.createSuperHero({
name: 'Batman',
type: 'hero',
});
const joker = superheroFactory.createSuperHero({
name: 'Joker',
health: 50,
type: 'villain',
});
batman.attacked(40); // Batman attacked: 40 -- 60
joker.attacked(40); // Joker attacked: 40 -- 10
joker.rampage(); // Joker restored health to 90