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

Understanding TypeScript and JavaScript's relationship

Now that you have a firm grasp of TypeScript's basic language concepts, you probably want to know how to migrate existing code in JavaScript to TypeScript, and what to look for while doing that. This is incredibly valuable if you already possess good experience with JavaScript, but you want to migrate some projects to TypeScript and you don't know how. Therefore, it's important to understand where existing JavaScript programs stand when translating them into TypeScript.

Let's move on to the next section to learn how JavaScript compares to TypeScript.

How does JavaScript compare to TypeScript?

If you are from a JavaScript background, you will find that learning TypeScript is not very far away from what you were doing. TypeScript adds types to JavaScript and, in reality, it wraps all JavaScript programs so that they are valid TypeScript programs by default. However, adding additional compiler checks may cause those programs not to compile as they did previously.

Therefore, you need to recognize the following concepts. Some JavaScript projects compile successfully. However, the same JavaScript projects may not type check. Those that type check represent a subset of all JavaScript programs. If you add more compiler checks, then this subset becomes smaller as the compiler will reject programs that do not pass this phase.

As a straightforward example, the following JavaScript program is also a valid TypeScript program by default, although no types are declared in the parameter name or the return type:

const isArray = (arr) => {
  return Array.isArray(a);
};

This program type checks correctly, so long as the noImplicitAny compiler flag is false.

Note

Although it is valid, it is not recommended in the long run as the compiler will infer the parameters as any type, which means that it will not type check them. When working on large-scale TypeScript projects, you should avoid those cases when you have implicit any types. If you don't, you lose many of the benefits of type safety.

Transitioning from JavaScript to TypeScript

A reasonable question you may have to answer when attempting to translate existing JavaScript code into TypeScript is this: How can you do this efficiently and how can you write correct types?

There are several techniques that you can use to perform that body of work, but in most cases, we can summarize it in a few words: divide and conquer:

  1. To begin with, you can start by dividing large pieces of JavaScript into smaller packages and files. This is to ensure you don't spend time only in one package.
  2. Then, start by renaming .js files as .ts files. Depending on the tsconfig flags, you will have some compilation errors, which is expected. Most of the compiler errors are for missing parameter types. For example, the following is a function that checks if the parameter is an object. You can easily use it in TypeScript, so long as the noImplicitAny compiler flag is unset:
    export const isObject = (o) => {
      return o === Object(o) && !Array.isArray(o) && 
        typeof o !== "function";
    };
  3. You may also want to enable the allowJs flag, which allows you to import regular JavaScript files in TypeScript programs, with no complaints from the compiler. For example, if you maintain a file named utilities.js, you can import it into TypeScript like so:
    import { isObject } from "./utilities";

    If you have imported from external libraries such as lodash or Rxjs, you may be prompted to download types for them. Usually, TypeScript will reference where those types are located. For example, for lodash, you should install it this way:

    npm install --save @types/lodash

In any other cases, you will have to follow the compiler leads and suggestions. Hopefully, if you have structured your programs so that they're in small and manageable pieces, then this process won't take much of your time.

Next, we will see whether design patterns can be used in JavaScript or whether it makes more sense to leave them as a typed language such as TypeScript.

Design patterns in JavaScript

When studying TypeScript design patterns and best practices, you may find yourself writing equivalent code in JavaScript for those examples. Although you can technically implement those patterns in JavaScript, the lack of types and abstractions makes learning those concepts less appealing.

For example, while using interfaces as parameters, we can change the implementation logic at runtime, without changing the function signature. This is how the strategy design pattern works, as will be explained in Chapter 5, Behavioral Design Patterns.

With JavaScript, we cannot use interfaces, so you may have to rely more on Duck Typing, property checks, or runtime assertions to verify that a particular method exists in an object.

Duck Typing is a concept where we are only interested in the shape of an object (property names or runtime type information) when we try to use it for a particular operation. This is because, in a dynamic environment such as JavaScript, there are only runtime checks to ensure the validity of operations. For example, let's say we have a function that accepts a logger object, which logs events into a stream, and an emailClient object by name and checks if certain methods are available before calling them:

function triggerNotification(emailClient, logger) {
    if (logger && typeof logger.log === 'function') {
        logger.log('Sending email');
    }
    if (emailClient && typeof emailClient.send === 
      'function') {
        emailClient.send("Message Sent")
    }
}

So long as the log and send properties exist in those objects and they are functions, then this operation will succeed. There are many ways that this can go wrong, though. Look at the following call to this function:

triggerNotification({ log: () => console.log("Logger call") }, { send: (msg) => console.log(msg) });

When you call the function this way, nothing happens. This is because the order of the parameters has changed (swapped) and log or send are not available as properties. When you provide the right shape of objects, then the call succeeds:

triggerNotification({ send: (msg) => console.log(msg) }, { log: () => console.log("Logger call") });

This is the correct output of this program:

> Logger call
> Message Sent

With the correct arguments passed into the triggerNotification function, you will see the aforementioned output of the console.log command.

Duck Typing has a similar counterpart to TypeScript, and it's called structural typing.

This is what is enforced during static analysis, and it means that when we have two types (A and B), then we can assign B to A if B is a subset of A. For example, look at the following logger object assignment:

interface Logger {
  log: (msg: string) => void;
}
let logger: Logger;
let cat = { log: (msg: string) => console.log(msg) };
logger = cat;

Here, A is logger of the Logger type and B is of the {log: (string) => void} type. Because type B is equivalent to A, this assignment is valid. Structural typing is a very important concept when learning TypeScript. Wewill see more examples throughout this book.

TypeScript and JavaScript have a close relationship, and Typescript will continue to be a superset of JavaScript for the time being. Now, let's learn how to use the code examples in this book.