Contents

    Guides

    Common Design Patterns in TypeScript

    Published on

    October 31, 2022
    Common Design Patterns in TypeScript

    In software design, a design pattern is a general solution to a commonly occurring problem. Design patterns are mechanisms for thinking about how different objects in your code will interact. They’re not specific code implementations. Instead, they’re templates for how those interactions might work.

    Design patterns address challenges that arise frequently when working with code and reduce the time spent on repeatedly solving the same problems.

    Design patterns are popularly classified based on their purpose, which can be creational, structural, and behavioral. In this article, we will cover some of the most commonly used creational and structural design patterns in TypeScript.

    Creational Patterns

    Creational design patterns are patterns primarily concerned with the process of object creation. They abstract the object instantiation process and make a system independent of how objects are created, composed, and represented.

    Creational patterns are mainly used in evolving systems that depend more on object composition than class inheritance. Examples include the factory pattern, the singleton pattern, etc.

    Factory Method Pattern

    The Factory method pattern, also known as the virtual constructor design pattern, is a creational pattern that loosens the coupling of an application by separating its construction code from the code that uses the application.

    The factory pattern creates a general “factory/creator” class and allows its subclasses to choose the kind of object they want to instantiate.

    This pattern is implemented by creating an interface all the products will follow, then, creating a general abstract creator class for all other creators (concrete creators) to extend. Finally, creating concrete products that implement the product interface.

    Below is a code example of a chair factory producing wooden and plastic chairs.

    
    // Product interface
    interface Chair {
    	hasFourLegs(): void;
    	hasBackRest(): void;
    }
    
    // Creator class
    abstract class ChairCreator {
    	abstract createChair(): Chair;
    
    	addFeature(): void {
    		const product = this.createChair();
    		product.hasBackRest();
    		product.hasFourLegs();
    	}
    }
    
    // Concrete Creator
    class WoodenChairCreator extends ChairCreator {
    	createChair(): Chair {
    		return new WoodenChair();
    	}
    }
    
    // Concrete Creator
    class PlasticChairCreator extends ChairCreator {
    	createChair(): Chair {
    		return new PlasticChair();
    	}
    }
    
    // Concrete product
    class WoodenChair implements Chair {
    	hasFourLegs(): void {
    		console.log('Has four wooden legs');
    	}
    
    	hasBackRest(): void {
    		console.log('Has a wooden back rest');
    	}
    }
    
    // Concrete product
    class PlasticChair implements Chair {
    	hasFourLegs(): void {
    		console.log('Has four plastic legs');
    	}
    
    	hasBackRest(): void {
    		console.log('Has a plastic back rest');
    	}
    }
    
    const clientCode = function (chair: ChairCreator) {
    	chair.addFeature();
    };
    //Creating products
    clientCode(new WoodenChairCreator());
    clientCode(new PlasticChairCreator());
    

    Implementing this pattern allows you to extend your codebase without breaking it. For example, add any type of chair, like a leather chair.

    This pattern should only be used when the type of objects your code will use is unknown because the pattern makes it easy to extend the product construction code independently from the rest of the application.

    Although this pattern allows you to avoid tight coupling between the creator class and the concrete products and maintains the single responsibility and open/closed principle, the code can become very complicated due to the number of subclasses needed to implement the pattern.

    Singleton Pattern

    The singleton pattern is a creational pattern that allows you to restrict access to a shared resource by ensuring that a class can only be instantiated once and providing a global point of access to it.

    The singleton pattern makes the class constructor private because, by default, the constructor will always return a new object, defeating the whole idea of the pattern.

    Then create a static method to check if the class has already been instantiated. If it has been instantiated before, it returns the already created instance; if it hasn’t, it creates an instance of the class and saves it to a static instance variable.

    Implementing the Singleton pattern in TypeScript, you’d have:

    
    class Singleton {
    	private static instance: Singleton;
    
    	static getInstance() {
    		if (!Singleton.instance) {
    			Singleton.instance = new Singleton();
    		}
    
    		return Singleton.instance;
    	}
    
    	private constructor() {}
    
    	doSomething(): void {
    		console.log('Doing something...');
    	}
    }
    
    const singleton = Singleton.getInstance();
    

    Although the Singleton pattern provides a global access point to the only instance of a class; it is a very controversial pattern and is considered an anti-pattern by many engineers who advise against using it because it violates the single-responsibility principle.

    The pattern also doesn't work well in multi-threaded environments as many components may be trying to access the shared resources simultaneously.

    Structural Patterns

    Structural design patterns are patterns primarily concerned with how objects are composed to form larger structures.

    They are mainly used for making independently developed class libraries work together.

    Examples include the adapter pattern, the facade pattern, etc.

    Adapter Pattern

    The adapter design pattern, also known as the wrapper design pattern, is a structural pattern that allows incompatible interfaces to interact by creating an adapter. An adapter is a middle layer class that serves as a bridge or translator between two incompatible interfaces.

    An adapter works by implementing an interface of one of the incompatible objects and, through that interface, calls the methods of the second object and then passes the request to the second object in a format it would understand.

    For context, assume you need to plug a three-pin plug into a two-pin wall socket. You would need a socket “adapter” that “implements” a two-pinned system and exposes a three-pinned socket, allowing you to plug your initial three-pin plug. This is how the adapter pattern works too.

    For example, say you built an application that accepts data in XML format. Later on, you need to consume data from a third-party library that sends JSON data. Changing the code of your initial application might break some things; the better way to approach this would be to use the adapter pattern.

    Implementing this scenario in code, you’d have:

    
    // Adaptee Interface
    interface JsonInterface {
    	recieveJson(): void;
    }
    
    // Target Interface
    interface XmlInterface {
    	recieveXml(): void;
    }
    
    //Adaptee class
    class JsonData implements JsonInterface {
    	recieveJson() {
    		console.log('Receiving JSON Data...');
    	}
    }
    
    //Target class
    class XmlData implements XmlInterface {
    	recieveXml() {
    		console.log('Receiving XML Data...');
    	}
    }
    
    //creating the adapter
    class JsonToXmlAdapter implements XmlInterface {
    	data: JsonInterface;
    
    	constructor(data: JsonInterface) {
    		this.data = data;
    	}
    
    	recieveXml(): void {
    		console.log('Converting JSON to XML...');
    		this.data.recieveJson();
    	}
    }
    
    const newData = new JsonData();
    
    //Instantiating the adapter
    const jsonAdapter = new JsonToXmlAdapter(newData);
    //Using the adapter
    jsonAdapter.recieveXml();
    

    Recall that your application only accepts XML data, but it needs to accept JSON data too. Therefore, your application is the target, and the application that serves JSON data is the adaptee. So you create a JSON to XML adapter which implements the target’s interface and calls the adaptee’s method safely. Therefore, giving your application a way to receive JSON data without changing your source code.

    Although this pattern obeys the single responsibility and open/close principle, it can increase the overall complexity of your code. So it should only be used when you need to use an already existing class with an incompatible interface with the rest of your code.

    Facade Pattern

    The Facade Pattern is a structural pattern that introduces a facade object that offers a simplified interface to a complicated set of classes like a framework or library, reducing communication and dependencies across subsystems.

    The facade object works by taking parts of complex classes required for an implementation to work and calling them inside it.

    For example, assume you are creating an E-commerce application and implementing the logic for placing an order. First, the customer has to be logged in, then the product the customer wants has to be in the store, and finally, the customer has to have enough money to purchase the product. Each of these processes is handled by three different classes, and these classes might contain other methods that are not relevant to the client for the placing-order process.

    Implementing the facade pattern by taking all the required methods and creating a facade object would be optimal for this scenario.

    Implementing this scenario in code, you’d have:

    
    class Auth {
    	isLoggedIn() {
    		console.log('User is Logged in');
    		return true;
        // Real Business Logic
    	}
    
      otherMethod() {
       console.log('Method')
      }
    }
    
    class StoreInventory {
    	productInStore() {
    		console.log('Product is available');
    		return true;
        // Real Business Logic
    	}
    
      otherMethod() {
       console.log('Method')
      }
    }
    
    class Accounts {
    	sufficientBalance() {
    		console.log('Customer has sufficient balance');
    		return true;
        // Real Business Logic
    	}
       
      otherMethod() {
       console.log('Method')
      }
    
    }
    
    class PlaceOrderFacade {
    	private auth: Auth = new Auth();
    	private checkInventory: StoreInventory = new StoreInventory();
    	private balance: Accounts = new Accounts();
    
    	placeOrder() {
    		if (this.auth.isLoggedIn() && this.checkInventory.productInStore() && this.balance.sufficientBalance()) {
    			console.log('Order placed successfully');
    		} else {
    			console.log('Something went wrong: Order not placed successfully');
    		}
    	}
    }
    
    const store = new PlaceOrderFacade();
    
    store.placeOrder();
    

    The facade pattern should only be used when you need a simpler and straightforward interface to complex subsystems or when you want to structure subsystems into layers.

    Although the facade pattern allows you to isolate your code from a complex subsystem, a facade object can become a god object coupled to all of an application’s classes.

    Conclusion

    In this article, you learned about the factory method, singleton, adapter, and facade design patterns implemented in TypeScript. Having these design patterns in your development arsenal will help you solve some common design problems and communicate effectively with other developers in collaborative projects.

    Data-rich bug reports loved by everyone

    Get visual proof, steps to reproduce and technical logs with one click

    Make bug reporting 50% faster and 100% less painful

    Rating LogosStars
    4.6
    |
    Category leader

    Liked the article? Spread the word

    Continue reading

    No items found.

    Put your knowledge to practice

    Try Bird on your next bug - you’ll love it

    Try Bird later, from your desktop

    Bird Call to action parrot
    By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.