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

Installing and using the code examples

When you download and install the source code that accompanies this book, you will find that the files are structured like a typical TypeScript project. We have included all the libraries and configurations you need to evaluate all the examples within the command line or via VSCode. It's useful to know what libraries are included in the examples and what problems they solve. Understanding the different tsconfig parameters that determine the behavior of the tsc compiler is helpful. You also need to be aware of how to run or debug the unit tests using Jest.

This section covers the following topics:

  • Explaining the libraries used in this book
  • Understanding the tsconfig configuration
  • Running the unit tests

Let's get started.

Reviewing the libraries included in the code examples

We have included several references from external libraries in this book's source code. Our aim is to help you review several of the design patterns within a specific use case. Here is an overview of what they are and what problems they solve:

  • React: React is the most popular library for creating user interfaces right now, and it promotes some useful patterns such as composition, component factories, and higher-order components. We will explain the usage of TypeScript with React in Chapter 2, TypeScript Core Principles.
  • Express: When building web services using TypeScript in Node.js, we want to use a minimal web framework. Express is the most stable choice when creating Node.js applications because it promotes modularity and performance. You will learn more about how to use TypeScript in the server in Chapter 2, TypeScript Core Principles.
  • immutable.js: This is a library that provides immutable data structures and the relevant utility methods for working with them in an efficient way. Immutability is a concept that we use quite frequently in functional programming, where we do not allow objects to be modified or altered once they have been created. We will learn more about immutability in Chapter 6, Functional Programming with TypeScript.
  • fp-ts: This is a library that exposes functional programming abstractions such as Monads, Options, and Lens. We will learn more about functional programming in Chapter 6, Functional Programming with TypeScript.
  • rx.js: This is a library that offers reactive programming abstractions such as Observables in a nice API. Using Observables can help us develop scalable and resilient applications. You will learn more about Observables in Chapter 7, Reactive Programming with TypeScript.
  • inversify.js: This is a lightweight inversion of control container, or IOC for short. We use IOC to handle object instantiation and lifetime guarantees, as well as to apply Single Responsibility, Open-Closed, Liskov-Substitution, Interface Segregation, and Dependency Inversion (SOLID) principles to our abstractions. We are going to explain more about these SOLID principles in Chapter 8, Developing Modern and Robust TypeScript Applications.

Using libraries is an excellent way to promote reusability and reliability. Quite often, when developing enterprise software, there is already a stack preconfigured for you, unless you are exceptionally fortunate to work in greenfield projects.

Next, we will learn how to configure the TypeScript compiler using tsconfig.json.

Understanding the tsconfig.json file

When you have a TypeScript source code inside a folder, the Typescript compiler needs to be able to find those files and compile them with some specific flags. Using a tsconfig.json or a jsconfig.json file determines the configuration-specific behavior of the compiler.

In most cases, you will only need one tsconfig.json file to manage all source code, but this is not a requirement. In this book, we will use a more flexible approach when compiling the source code examples.

We have a base tsconfig.json file that pertains to all the common compiler flags for all the chapters in this book. Then, each chapter will contain its own tsconfig.json, which inherits from the base config.

To understand what these flags are and what they do, let's describe them briefly now:

  • module: Modules define how imports and exports work. In this book, we are using CommonJS, which is the format used for Node.js projects. This means that the code generation will create the relevant require statements. For example, you can inspect the compiled code of the code generation inside the dist folder.
  • target: This specifies the actual code generation target, such as ES6, ES2017, ESNEXT, and so on. This means that some features may not work in all environments, such as Node.js or older browsers. In this project, we will use ES5, which is the lowest common denominator; it has great support.
  • noImplicitAny: This prevents the program from compiling when TypeScript infers the type as any. This happens relatively often when you define a function without specifying the types for the parameters. For example, the following program (degToRad.ts) does not compile when this flag is true:
    const degToRad = (degree): number => (degree * Math.PI) / 180;
    >  npx tsc --build chapters/chapter-1_Getting_Started_With_Typescript_4
    chapters/chapter-1_Getting_Started_With_Typescript_4/degToRad.ts:1:19 - error TS7006: Parameter 'degree' implicitly has an 'any' type.
     
    1 const degToRad = (degree): number => (degree * Math.PI) / 180;
  • strictNullChecks: This rule makes undefined and null checks more prevalent. Any time the compiler infers a type as undefined, it will follow any code that does not ensure that null is left unchecked, and it will raise an error.
  • experimentalDecorators and emitDecoratorMetadata: In some examples, especially when using Inversify.js, we use decorators, which are experimental features of JavaScript. Decorators are a very interesting concept, and they also have a relevant design pattern for using classes. Enabling these two flags is a requirement with TypeScript.
  • sourceMap: This enables source maps that are used when debugging TypeScript code. This will allow, for example, VSCode or the browser to show the original uncompiled Typescript code when stopping at breakpoints.

    There are also many more compiler flags available that can tweak different aspects of the system. These options usually tweak more specific aspects of the compiler by customizing the restrictiveness of the type checks. For example, using strictBindCallApply or strictFunctionTypes may introduce more type checks for the Bind, Call, Apply, or Function types. Before enabling any extra flags, it is recommended that you achieve consensus with your colleagues to avoid any confusion.

Running the unit tests

As we mentioned previously, you can run unit tests using the Jest runner. This is a popular testing framework for TypeScript and JavaScript projects as it is easy to get started and it has good integrations with major frameworks. Here, we have provided configuration options for running the unit tests right from VSCode.

To run the tests, you'll have to execute the following command in the console:

npm run-script test

For example, there is a file named mul.ts that includes a function for multiplying two numbers:

mul.ts

function mul(a: number, b: number) {
  return a * b;
}
export default mul;

Then, we also have the test file for this function, which has the same filename but with a test.ts extension:

mul.test.ts

import mul from "./mul";
test("multiplies 2 and 3 to give 6", () => {
  expect(mul(2, 3)).toBe(6);
});

When you execute these test cases, you will see the runner results:

npm test
 
> [email protected] test TypeScript-4-Design-Patterns-and-Best-Practices
> jest
PASS  chapters/chapter-1_Getting_Started_With_Typescript_4/mul.test.ts
   multiplies 2 and 3 to give 12 (1 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

We will frequently use Jest to verify some assumptions of design patterns. For example, we will test whether the Singleton design pattern uses only one instance and does not create more, or whether the Factory pattern constructs objects with the right type and nothing else. Writing good unit test cases is often a requirement before releasing code to production, so it's crucial to always test your abstractions promptly.