Design Patterns in TypeScript

Some background – The Factory Incentive

continuing our series of design patterns.

When switching from plain Javascript to Typescript a new big world of design patterns that were not relevant till now is revealed, as you did not have types, generics interfaces abstract classes and more.

I see a lot of javascript programmers that switch to typescript and keep writing code as if they are still in the javascript world, they basically do not use any of Typescript strong features that may create a more reusable and maintainable code.

Typescript factory pattern
Typescript factory pattern

Defining The Factory Pattern:

We will use a common example of a program that creates planes – it can create an Airbus, Boing, or Lockheed Martin.

The steps we should take:

  • Define an interface representing a plane.
  • Define Classes that implements our plane interface.
  • Create a Static Function/ Class that initiates planes according to some query.
// Plane.interface.ts
export interface Plane {
    fly(): string;
    land(): string;
}
// airbus-plane.ts
import {Plane} from "./plane.interface";
export class AirbusPlane implements Plane {

    constructor(){
        this._name = 'AirBus';
    }
    private _name: strig;

    get name(){
        return this._name;
    }

    land(): string {
        return `${this.name} is Landing`;
    }

    fly(): string {
        return `${this.name} is Flying ***`;
    }
}
// boing-plane.ts
import {Plane} from "./plane.interface";
export class BoeingPlane implements Plane {

    constructor(){
        this._name = 'Boeing';
    }
    _name: string;

    get name(){
        return this._name;
    }

    land(): string {
        return `${this.name} is Landing`;
    }

    fly(): string {
        return `${this.name} is Flying ***`;
    }
}

We have an interface of Plane and concrete classes that implement it, by the way, some languages best practices tell you to the interface should start with an “I” such as “IPlane”.

For now, if we would like to create a Boeing plane or an Airbus plane we need to use the “new” keyword and to be coupled to the creation of the concrete class such as:

const plane: Plane = new BoeingPlane();

We can try to make it a little more elegant if we switch the type to the interface “Plane” but we will still need to know how to create a Boeing plane or an Airbus plane…. this will make our module hard to extend adding new planes as different planes from different manufacture keeps on arriving.

The Null Pattern

Next step in our module will be to create some kind of strange plane the “NullPlane” aka the null pattern:

import {Plane} from "./plane.interface";
export class NullPlane implements Plane {

    constructor(){
        this._name = 'none';
    }
    _name: string;

    get name(){
        return this._name;
    }

    land(): string {
        return ``;
    }

    fly(): string {
        return ``;
    }
}

What is the purpose of this plane? imagine a scenario where some new developers in the company misunderstand the different plane types and try to create a new unknown plane, this will probably result in a runtime error. I use the “NullObject” pattern to prevent runtime bugs by creating a plane that does nothing – so the user of our factory will not “crash” but just create a useless plane – we can now use it as a default plane when a user tries to create a new plane without the necessary knowledge.

The Plane Factory – Switch case

Now it’s time to create our first Factory Class that knows how to create planes correctly – and if it doesn’t it can just return the null plane:

import {Plane} from "./plane.interface";
import {NullPlane} from "./null-plane";
import {BoeingPlane} from "./boing-plane";
import {AirbusPlane} from "./airbus-plane";
const nullPlane = new NullPlane();

export type Planes = 'boeing' | 'airbus';
export class PlaneFactory{

    static getPlaneInstance(type: Planes): Plane{
        switch (type){
            case 'boeing': return new BoeingPlane();
            case 'airbus':  return new AirbusPlane();
            default : return nullPlane;
        }
    }
}

This is usually the only place in our codebase where we will happily use the switch-case statement.

Notice that since the null plane has no meaning I can save some memory if I use only the same reference and keep returning it each time someone fails to create a proper plane.

Avoding If statements in the Factory pattern

We can upgrade this factory method to be less bounded to the types of the actual planes by creating an indes.ts that exports all of our planes

// Index.ts
 export * from './boingplane';
 export * from './airbaseplane';
 export * from './nullplane';

This file will allow us to import all of the plane objects as on a big array and the key of the array will be the name of the class we are exporting.

import * as planes from './planes/index.ts'
class PlaneFactory {
    static getInstance<T>(name: string, ...args: any[]) : T {
        const instance = Object.create(planes[name].prototype);
        instance.constructor.apply(instance, args);
        return <T> instance;
    }
}

const boeingPlane: Plane = PlaneFactory.getInstance<Plane>('BoeingPlane');
console.log(boeingPlane.fly());

via GIPHY

Breaking the factory pattern in 2 ways – Design Patterns in TypeScript

Yoni Amishav


Tech lead, blogger, node js Angular and more ...


Post navigation


2 thoughts on “Breaking the factory pattern in 2 ways – Design Patterns in TypeScript

  1. wanted to avoid if/switch in my factory, your generic implemetation has resolved this issue, thanks.
    It will be helpful if you could add unit test for the same.

Leave a Reply

Free Email Updates
Get the latest content first.
We respect your privacy.
%d