Bulletproof TypeScript - strict mode for Enterprise scale Applications
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:
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.
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.
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.
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
alwaysStrict rule enables ES strict mode for the whole TypeScript codebase. It adds the "use strict" line at the beginning of every source file.
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:
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.
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
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
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.
This rule is self-explanatory. The keyword
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.
One of the most common job interview questions for a position of a frontend developer is "What is function's
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
The more tricky part comes when we start to mix functions with classes:
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.
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.
apply are invoked with arguments of the correct type:
strictBindCallApply is a kind of archaic rule. From my experience, you don't see many
apply usage, at least in projects using frameworks like React and Vue.
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
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.