Bulletproof TypeScript - strict mode for Enterprise scale Applications

Picture of a computer.
Photo by Carl Heyerdahl on Unsplash

I feel that developers who use TypeScript on a daily basis do not appreciate its compiler. Its main responsibility is to do its magic and change TypeScript code into JavaScript. Did you know that it also automatically helps you to find bugs in your code? In addition, it also allows you to keep your code cleaner.

TypeScript compiler offers a set of rules that verifies your code during the transpilation process. No additional tools are needed and there are no other processes running in the background. The TypeScript team puts a lot of effort to make rule checks during the whole transpilation to JavaScript magic.

This set of rules is often called "strict mode". It represents a collection of automatic checks that tighten the verification of your TypeScript code. The be more precise, the list of the rules is:

  • alwaysStrict,
  • strictBindCallApply,
  • strictFunctionTypes,
  • strictNullChecks,
  • strictPropertyInitialization,
  • noImplicitAny,
  • noImplicitThis,
  • useUnknownInCatchVariables.

In the past few months, me and my team have introduced a strict mode in a large enterprise-scale React application. I would like to share our experience and what we have learned during the whole process. I will present each rule of the strict mode set, so you could also start using it as soon as possible.

Automatic checks

Creating an enterprise-scale application always comes with the challenge of allowing it to grow while keeping it stable. Every tool that helps your application be less buggy is worth using. Especially if it is already built into your programming language.

You may wonder why the TypeScript compiler is that important, you could use an additional tool like Eslint, Tslint(although it has been deprecated), or others. The answer is performance. Rules built into the compiler are verified during transpilation from TypeScript to JavaScript. They are also integrated into the language so you can be sure that they work as intended. Taking that into consideration automatic rules from the TS compiler bring lots of benefits to every project.

Divide and conquer

Enabling strict mode in a large project is quite a challenge. It's really hard to do it in one commit or even in one sprint. I recommend using the most basic programming pattern Divide and conquer. You can achieve it in many different ways, the most popular are:

  • splitting code into separate libraries,
  • monorepo solutions like Nx, yarn workspace, Bit, Turborepo,
  • enabling rules one by one for specific directories.

TypeScript compile

The first two mechanisms require lots of work and reinventing project architecture. The last one is the easiest to use right away. You can create a new config file e.g. tsconfig.rule-check.json and enable specific rules one by one:

File tsconfig.json with some of the rules turned on
File tsconfig.json with some of the rules turned on

This config extends the base one, so it works the same as your previous transpilation, but it enables additional rules. Now you can run TypeScript compile using this specific configuration

The command for running TypeScript check
The command for running TypeScript check

You can also run it in watch mode, which is very helpful:

The command for running TypeScript check with watch mode enabled
The command for running TypeScript check with watch mode enabled

ECMAScript strict mode

The first rule that I would like to present is related to the JavaScript strict mode. ECMAScript introduced strict mode in version ES5. It's a restricted variant of JavaScript. It changes the behavior of many built-in features making the whole language less sloppy.

alwaysStrict

The alwaysStrict rule enables ES strict mode for the whole TypeScript codebase. It adds the "use strict" line at the beginning of every source file.

Example code showing JavaScript strict mode
Example code showing JavaScript strict mode

Layer architecture

The next couple of rules are especially beneficial when you deal with large enterprise applications. In order to better understand the issues, I would like to present the concept in a more detailed way.

As a project grows it faces new challenges:

  • new changes introduce regression,
  • dependencies between parts of an application,
  • multi-development teams working on one codebase.

One of the solutions is the layered architecture often jokingly called "The Lasagne architecture". Basically, the application is divided into layers each independent of the others. It allows clarity in responsibilities because layers handle different tasks and allow to split workflow better. Developers can work separately on every layer.

Types of software architecture
Types of software architecture

The issue with this architecture is that variables are passed through many different functions or services. For example, the layer responsible for fetching data retrieves it from an external source and then passes it to the layer that converts data for UI. Then the presentation layer receives data and handles the rendering process. Every layer consists of a couple of functionalities that call each other. It may be hard to debug code from start to end.

Application layers
Application layers

It can also create a situation where one layer expects a different API than the other provides. In my opinion, it's extremely important to verify the types exactly in multi-layer architecture, as it will reduce the number of bugs immensely. The list of rules that are important when you deal with the large layered applications:

  • strictFunctionTypes,
  • strictNullChecks,
  • strictPropertyInitialization.

strictFunctionTypes

This rule is a great example of why verifying types strictly will increase the stability of an application. Take a look at the code below:

Example code that can be fixed by rule strictFunctionTypes
Example code that can be fixed by rule strictFunctionTypes

You can see an obvious mistake here. In an application built with many layers, it is not that easy to catch.

Turning one the strictFunctionTypes rule will catch this error during the compilation process:

Example code with error from ts compiler
Example code with error from ts compiler

TS compiler shows that types are incorrect.

strictNullChecks

I guess lots of you heard of the "billion dollar mistake", an invention created in 1965 by scientist Tony Hoare who introduced the null reference to the language ALGOL. Later on, he criticized his invention and named it the most costly bug introduced in programming history. It is not surprising that TypeScript brings mechanisms to deal with this issue.

TypeScript by design allows assigning null to any variable of a specific type. It automatically assumes that the variable can be of exact type or null. The strictNullChecks rule changes these behaviors. It forces programmers to always explicitly declare the null type for a variable if they want to assign null to the variable. It changes the default behavior of the null type.

Example code showing issues with assigning null to a variable
Example code showing issues with assigning null to a variable

strictPropertyInitialization

Let's start with recollecting a popular programming practice:

You should not be able to create an instance of a class, if it is not initialized and ready to use.

That means that objects should be ready to use after it has been created. In other words, all of the object's properties, that are required for it to correctly work, should be assigned at the time of creation. This practice solves a lot of issues when we try to use an object that has undefined properties(is not ready to use):

Example code where an instance of a class is not ready after an object has been created
Example code where an instance of a class is not ready after an object has been created

Once again with the help comes the TypeScript compiler. Enabling the strictPropertyInitialization rule will catch all the places where a class property is not assigned at the declaration or in the constructor.

Error showing that property is not assigned
Error showing that property is not assigned

This rule has lots of value as not only does it help to find bugs it also fixes lots of issues with the architecture.

noImplicitAny

This rule is self-explanatory. The keyword any is very helpful when you transition your project from JavaScript to TypeScript. It helps speed up the process and allows to do it in iterations. After it is done there may be some places where you forgot to specify the type, like in the example below:

Code showing usage of any
Code showing usage of any

Enabling the noImplicitAny rule will ban the usage of implicit any in your project. Achieving this rule for the whole project is one of the biggest challenges a TypeScript programmer can face. It also brings the most benefits as the project gives no error of improper type use.

If you want to totally remove usage of any from your project you can use ESLint and its rule no-explicit-any.

noImplicitThis

One of the most common job interview questions for a position of a frontend developer is "What is function's this keyword in JavaScript?".

JavaScript functions are always executed in the context of an object. So this behaves differently base on how it is invoked. If you don't specify a context object for which a function is executed, that object is a global object. On the other hand, you can define it with the call or apply. I highly recommend reading more about the nuances of this behavior in JavaScript.

The more tricky part comes when we start to mix functions with classes:

Example code presenting issues with the usage of this keyword
Example code presenting issues with the usage of this keyword

You expect this in the function created in the collar method to refer to the instance of the Dog object, but it doesn't. It refers to the object in which the context function callMe will be executed.

Enabling the noImplicitThis rule will catch this issue:

Error showing problematic this usage
Error showing problematic this usage

In TypeScript and in the world of classes and Object-Oriented Programming the unusual behavior of this keyword may bring some confusion and unwanted bugs.

strictBindCallApply

Compiler verifies that JavaScript functions bind, call and apply are invoked with arguments of the correct type:

Example code showing improper usage of call
Example code showing improper usage of call

This strictBindCallApply is a kind of archaic rule. From my experience, you don't see many bind, call or apply usage, at least in projects using frameworks like React and Vue.

useUnknownInCatchVariables

This rule was introduced in one of the recent versions of TypeScript (version 4.4). It is really straightforward, we always assume that the error in the catch block is of the type we expect it to be:

Example of a simple 'try catch' block
Example of a simple "try catch" block

In fact, it is unknown, so in order to be sure, we need to test it.

Example code showing correct error verification
Example code showing correct error verification

Adding the if block code where we verify that error is an instance of the expected type fixes the issue. In real-life applications errors can come from different places, so we always have to check what the error we get represents.

Additional code quality rules

The compiler offers more than just verifying the strict mode. It has a set of additional built-in rules that checks your code for bugs and code quality. This article is part of a series of automatic code quality rules for the TypeScript compiler. If you want to read more about it follow the next article: Bulletproof TypeScript - code quality rules beyond strict mode

Summary

Frontend applications grow in size. It is getting harder to maintain and develop new features. Automatic verification which the TypeScript compiler offers is a great tool to help you catch bugs upfront and keep the quality of your code at a high level.

From my experience, the strict mode is really hard to introduce into the living project. Although, splitting the whole process into iterations and enabling rules one by one will help you turn it on. Trust me, strict mode pays up in the long run, it makes your codebase better. It allows to catch bugs in the development phase and increases the developer's experience for the whole team.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.