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 TypeScript 4

Understanding the basic language constructs of TypeScript is very valuable when learning design patterns. You will need to recognize valid TypeScript code and some of its features because it will help you define better typings for objects, as well as help you avoid mistakes. We will strive to provide small but consistent examples and use cases of TypeScript idioms and constructs for completeness.

The basic structure of a TypeScript program consists of statements or expressions. The following is a list of basic types that are partly associated with JavaScript runtime types:

  • Primitive types: These are number, string, Boolean, void, null, undefined, unknown, never, unique, bigint, and any values. To define or declare them, you need to write the name of the variable, followed by a semicolon (:) and its type. If you assign the wrong type, then the compiler will throw an error. Here is an example usage of those types (intro.ts):
    const one: string = "one";
    const two: boolean = false;
    const three: number = 3;
    const four: null = null;
    const five: unknown = 5;
    const six: any = 6;
    const seven: unique symbol = Symbol("seven");
    let eight: never; // note that const eight: never cannot happen as we cannot instantiate a never
  • Enums: They allow us to create named constants, such as the following:
    enum Keys {
      Up,
      Down,
      Left,
      Right,
    }
    let up: Keys = Keys.Up;

    You can enforce a compiler optimization with enums to make them constant, thus eliminating any unused information:

    const enum Bool {
      True,
      False,
    }
     
    let truth: Bool = Bool.True;
  • Array and tuples: Arrays represent a collection of items of the same type, and they can have a variable size:
    const arr: number[] = [1, 2, 3]; // array of numbers of any size

    Tuples represent a fixed array, with each element having a defined type:

    const tup: [number] = [1]; // tuple with one element of type number
  • Classes: These are typical Object-Oriented Programming (OOP) abstractions that allow us to define objects of a specific shape with properties, methods, and visibility modifiers. For example, here is a typical use case of a class:
    class User {
      private name: string;
      constructor(name: string) {
        this.name = name;
      }
     
      public getName(): string {
        return this.name;
      }
    }
     
    const user = new User("Theo");
    console.log(user.getName()); // prints "Theo"

    You can also define abstract classes (that is, regular classes) that cannot be instantiated. Instead, they need to be inherited as part of a parent-child relationship:

    abstract class BaseApiClient {
      abstract fetch(req: any): Promise<any>; /* must be implemented in sub-classes*/
    }
    class UsersClient extends BaseApiClient {
      fetch(req: any): Promise<any> {
        return Promise.resolve([]);
      }
    }
    const client = new UsersClient();
    client.fetch({url: '/users'});
  • Interfaces and types: Interfaces are abstractions that let you define the shape of an object and its properties, but without specifying an implementation. For example, we can define a Comparable interface like this:
    interface Comparable<T> {
        compareTo(o: T): number
    }

    Note that we are not defining an implementation for compareTo here, just its type. Interfaces in TypeScript can also have properties:

    interface AppConfig {
        paths: {
            base: string;
        };
        maxRetryCount?: number;
    }

    The question mark (?) after the name represents an optional parameter, so it's allowed to create a type with or without it:

    const appConfig: AppConfig = {
        paths: {
            base: '/',
        }
    }

    Type is a similar concept to interfaces but is a bit more flexible. You can combine a Type with another Type either as a union or as an intersection type:

    type A = 'A'; // type is 'A'
    type B = 'B'; // type is 'B'
     
    type C = A & B; /* type is never as there is nothing in common between A and C*/
    type D = C | "E"; // type is "E" as C is a never type
    type E = {
        name: string;
    }
    type F = E & {
        age: number;
    }
    let e: F = {
        name: "Theo",
        age: 20
    }

    Note

    As a rule of thumb, you should be declaring interfaces first. However, when you want to combine or create new types on the fly, then you should use types.

There are many other notable features of TypeScript that you will learn about throughout this book. Now, let's move on and learn how to handle input and output.

Working with input and output

Understanding how to read from input and write to output is one of the most fundamental skills of any programming language. Handling input and output operations with TypeScript depends primarily on where you use it. For example, when using TypeScript in a browser environment, you accept input from user interactions, such as when a user clicks on a button and submits a form or when you send an AJAX request to a server.

When using TypeScript in a server, you can read input values from command-line arguments or from the standard input stream (stdin). Subsequently, we can write values to the output stream, called the standard output stream (stdout). All these concepts are common to all computer environments.

As an example, let's take a case where we are using TypeScript with Node.js. We can use the following simple program to read from stdin and write to stdout:

inputOutput.ts

const stream = process.stdin;
setImmediate(function () {
  stream.push(null);
});
 
stream.pipe(process.stdout);

Then, you can invoke this program from the command line:

 echo "Hello" | npm run ts chapters/chapter-1_Getting_Started_With_Typescript_4/inputOutput.ts
Hello World

Working with streams exposes a different programming model, called reactive programming, where you are concerned about asynchronous data streams and events. You will learn more about asynchronous communication patterns in Chapter 7, Reactive Programming with TypeScript.

Useful TypeScript 4 features

The latest version of TypeScript (v4.2) offers a great list of features that help developers write type-safe programs and abstractions. For example, with TypeScript 4, we have the following:

  • Variadic tuple types: Tuples are interesting data structures as they represent fixed array types, where each element has a specific type. For example, we can model a point in 2D or 3D space as a tuple:
    type Point2d = [number, number];
    type Point3d = [number, number, number];
    const point1: Point2d = [1, 2];
    const point2: Point3d = [1, 2, 3];

    Before TypeScript 4, you could not pass a variadic type parameter for a tuple as the shape of the tuple had to be defined. Now, let's check out the following code:

    type NamedType<T extends unknown[]> = [string, ...T];
    type NamedPoint2d = NamedType<Point2d>;
    const point3: NamedPoint2d = ["Point: (1, 2)", 1, 2];

    Here, the type of NamedPoint2d is [string, number, number]. With this feature, we may have more compelling reasons to use tuples to model domain primitives.

  • Labeled tuples: Taking the previous example with the two tuples, we can also add names for each tuple element. This can improve documentation as you can clearly see the corresponding parameter for each item. For example, let's add labels to show the usage of x, y, and z coordinates:
    type Point2dL = [x: number, y: number];
    type Point3dL = [x: number, y: number, z: number]; 

    Labeled tuples are useful for documentation purposes; so, if you use tuples, you should also provide labels for them.

  • Template literal types: TypeScript 4 has added a new literal type for templates that allows us to combine types. This helps when defining new types out of existing ones and you want to avoid repetition. For example, we can model a Deck of Cards type using just two lines of code:
    type Suit = `${"Spade" | "Heart" | "Diamond" | "Club"}`;
    type Rank = `${"2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "Jack" | "Queen" | "King" | "Ace"}`
     
    type Deck = `${Rank} of ${Suit}`;

If you inspect the type of the Deck declaration, you will see that it enumerates the possible cards of a standard 53 deck of cards: 2 of Spade, 3 of Spade …, Ace of Club.

Now that we've introduced and understood TypeScript 4's features, let's learn how TypeScript and JavaScript are related to each other.