Bulletproof TypeScript - strict mode for Enterprise scale Applications

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:

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

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

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.

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.

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.

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:

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:

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.

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):

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.

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:

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:

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:

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:

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:

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

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.