Factory Pattern in TypeScript

Design patterns play an important part in software development and they do help us to design better software and write better code.

GoF

Back in 1994, a book was authored by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides that discusses 23 desgin patterns, titled Design Patterns: Elements of Resuable Object-Oriented Software. You may have heard of this book or the authors as Gang of Four (GoF).

The book and the outlined design patterns in them formtulate a solid base for today's software development and those patterns introduced and explained in 1994 are still applicable today and can be used in modern day applications.

Design Pattern types

Design patterns can be categorised into the following categories based on what and how 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 one of the creational software design patterns. This design pattern provides a generic interface to create objects. It works with a creator that produces products. When using the factory pattern, we are not required to use the new keyword because the instantiation of additional classes are done in the factory implementation.

When to use the factory pattern?

The factory pattern can be used in situations where the object creation process is complex or in situations where multiple object are created that share the same properties.

TypeScript implementation

The factory pattern could be implemented in any programming language, including vanilla JavaScript as well but I thought it'd be interesting to see how a TypeScript implementation would look like.

It goes without saying that should you want to get the JavaScript implementation, just transpile the TypeScript code to JavaScript, either to ES2015 or to ES5.

Factory example

First let's take a look at a simple factory pattern example - we are going to assume that we have a factory that allows us to create superheroes. In this setup we'd have a simple class:

// 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);
}

The implemention of this class is very straight forward:

// 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 to the next level now.

Let's assume that we are working on a game where we can create superheroes - a superhero can be either a good guy (aka a Hero) or a bad guy (aka a Villain). These two superheroes 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(`Hero attacked: ${attackValue} -- ${this.health}`);
    }
  }

  heal(healValue) {
    if (this.health + healValue > 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.90
      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(`Villain attacked: ${attackValue} -- ${this.health}`);
    }
  }

  heal(healValue) {
    if (this.health + healValue > 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 that we have the classes, we can 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 how in the above code we are doing specialised signatures - if a hero is created we return the Hero class and if a villain is created we return the Villain class. We need to do this to make sure that the methods from these classes are available. Leaving these two statements out would mean that the rampage() method would be inaccessible: Property 'rampage' does not exist on type 'Hero | Villain'. Property 'rampage' does not exist on type 'Hero'. This message means that TypeScript is assuming that joker is of type Hero, which is incorrect.

Finally we can go ahead and 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); // Hero attacked: 40 -- 60
joker.attacked(40); // Villain attacked: 40 -- 10
joker.rampage(); // Joker restored health to 90