Book Image

TypeScript 4 Design Patterns and Best Practices

By : Theofanis Despoudis
Book Image

TypeScript 4 Design Patterns and Best Practices

By: Theofanis Despoudis

Overview of this book

Design patterns are critical armor for every developer to build maintainable apps. TypeScript 4 Design Patterns and Best Practices is a one-stop guide to help you learn design patterns and practices to develop scalable TypeScript applications. It will also serve as handy documentation for future maintainers. This book takes a hands-on approach to help you get up and running with the implementation of TypeScript design patterns and associated methodologies for writing testable code. You'll start by exploring the practical aspects of TypeScript 4 and its new features. The book will then take you through the traditional gang of four (GOF) design patterns in their classic and alternative form and show you how to use them in real-world development projects. Once you've got to grips with traditional design patterns, you'll advance to learning about their functional programming and reactive programming counterparts and how to couple them to deliver better and more idiomatic TypeScript code. By the end of this TypeScript book, you'll be able to efficiently recognize when and how to use the right design patterns in any practical use case and gain the confidence to work on scalable and maintainable TypeScript projects of any size.
Table of Contents (14 chapters)
1
Section 1: Getting Started with TypeScript 4
4
Section 2: Core Design Patterns and Concepts
8
Section 3: Advanced Concepts and Best Practices

Introducing Unified Modeling Language (UML)

You now know how to work with VSCode and have a firm understanding of its code base and some examples. We will complete this chapter by learning about UML and how we can utilize it to study design patterns. We will focus on a limited set of UML, specifically class diagrams, since this is the traditional way to depict design patterns; plus, they are straightforward to comprehend.

What is UML?

UML is a standardized way of modeling software architecture concepts, as well as interactions between systems or deployment configurations. Nowadays, UML covers more areas, and it's fairly comprehensive. It came as a result of a consolidation of similar tools and modeling techniques, such as use cases, the Object Modeling Technique (OMT), and the Booch Method.

You don't really need to know all the ins and outs of UML, but it is really helpful when you're learning about design patterns. When you first learn about design patterns, you want to have a holistic overview of the patterns, irrespective of the implementation part, which will differ from language to language. Using UML class diagrams is a perfect choice for modeling our patterns in a design language that everyone can understand with minimal training.

Let's delve into more practical examples using TypeScript.

Note

Although UML diagrams have a long history in software engineering, you should use them carefully. Generally, they should only be used to demonstrate a specific use case or sub-system, together with a short explanation of the architecture decisions. UML is not very suitable for capturing the dynamic requirements of very complex systems because, as a visual language, it is only suitable for representing high-level overviews.

Learning UML class diagrams

UML class diagrams consist of static representations of the classes or objects of a system. TypeScript supports classes and interfaces, as well as visibility modifiers (public, protected, or private) so that we can leverage those types to describe them with class diagrams. Here are some of the most fundamental concepts when studying class diagrams:

  • A class represents a collection of objects with a specific structure and features. For example, the following Product class looks like this:
    class Product {}

    This corresponds to the following diagram:

Figure 1.7 – Class representation

Figure 1.7 – Class representation

  • An interface is usually attached to a class and represents a contract that the class adheres to. This means that the class implements this interface:
    interface Identifiable<T extends string | number>{
        id: T
    }
    class Product implements Identifiable<string> {
        id: string
        constructor(id: string) {
            this.id = id;
        }
    }

    This corresponds to the following diagram. Notice the placement of the interface clause on top of the class name within the left shift (<<) and right shift (>>) symbols:

Figure 1.8 – Interface representation

Figure 1.8 – Interface representation

  • An abstract class represents an object that can't be directly instantiated:
    abstract class BaseApiClient {}

    This corresponds to the following diagram. The name of the class is in italics:

Figure 1.9 – Abstract class representation

Figure 1.9 – Abstract class representation

  • An association represents a basic relationship between classes, interfaces, or similar types. We use associations to show how they are linked with each other, and this can be direct or indirect. For example, we have the following models for Blog and Author:
    class Blog implements Identifiable<string> {
        id: string;
        authorId: string;
        constructor(id: string, authorId: string) {
            this.id = id;
            this.authorId = authorId;
        }
    }
    class Author {}

    This corresponds to the following diagram. Blog is connected to Author with a line:

Figure 1.10 – Association representation

Figure 1.10 – Association representation

Notice that because the Author class here is not being passed as a parameter, it is referenced from the authorId parameter instead. This is an example of indirect association.

  • An aggregation is a special case of association when we have two entities that can exist when one is missing or not available. For example, let's say we have a SearchService that accepts a QueryBuilder parameter and performs API requests on a different system:
    class QueryBuilder {}
    class EmptyQueryBuilder extends QueryBuilder {}
    interface SearchParams {
      qb?: QueryBuilder;
      path: string;
    }
     
    class SearchService {
      queryBuilder?: QueryBuilder;
      path: string;
     
      constructor({ qb = EmptyQueryBuilder, path }: 
        SearchParams) {
        this.queryBuilder = qb;
        this.path = path;
      }
    }

    This corresponds to the following diagram. SearchService is connected to QueryBuilder with a line and a white rhombus:

Figure 1.11 – Aggregation representation

Figure 1.11 – Aggregation representation

In this case, when we don't have a QueryBuilder or the class itself has no queries to perform, then SearchService will still exist, although it will not actually perform any requests. QueryBuilder can also exist without SearchService.

Composition is a stricter version of aggregation, where we have a parent component or class that will control the lifetime of its children. If the parent is removed from the system, then all the children will be removed as well. Here is an example with Directory and File:

class Directory {
  files: File[];
  directories: Directory[];
  constructor(files: File[], directories: Directory[]) {
    this.files = files;
    this.directories = directories;
  }
 
  addFile(file: File): void {
      this.files.push(file);
  }
  addDir(directory: Directory): void {
    this.directories.push(directory);
  }
}

This corresponds to the following diagram. Directory is connected to File with a line and a black or filled rhombus:

Figure 1.12 – Composition representation

Figure 1.12 – Composition representation

  • Inheritance represents a parent-child relationship when there is one or more sub-classes that inherit from base classes (also known as a superclass):
    class BaseClient {} 
    class UsersApiClient extends BaseClient {}  

    This corresponds to the following diagram. UsersApiClient is connected to BaseClient with a line and a white pointed arrow:

Figure 1.13 – Inheritance representation

Figure 1.13 – Inheritance representation

  • Visibility is related to attributes that the class contains and how they are accessed. For example, we have an SSHUser class that accepts a private key and a public key:
    class SSHUser {
      private privateKey: string;
      public publicKey: string;
     
      constructor(prvKey: string, pubKey: string) {
        this.privateKey = prvKey;
        this.publicKey = pubKey;
      }
     
      public getBase64(): string {
        return Buffer.from(this.publicKey).toString
          ("base64");
      }
    }

    This corresponds to the following diagram. SSHUser contains two properties and one method. We use a minus (-) for private visibility and a plus (+) for public visibility:

Figure 1.14 – Visibility

Figure 1.14 – Visibility

Here, we can see that the methods are separated by a horizontal bar for visibility.

We can also add notes or comments to class diagrams, although it's not very clear if they should be included in the code:

Figure 1.15 – Comments representation

Figure 1.15 – Comments representation

The main difficulty when using class diagrams is not drawing them on a piece of paper, but rather how to properly model the domain classes and relationships in a sound manner. This process is often iterative and involves interacting with several domain experts or knowledgeable stakeholders. In Chapter 8, Developing Modern and Robust TypeScript Applications, we are going to learn how domain-driven design can help us with modeling business rules.