Bulletproof TypeScript - code quality rules beyond strict mode

TypeScript compiler is known for its strict mode. It enables more accurate type checking. Did you know that it has more of these automatic rules than just strict mode? TypeScript team with each release offers not only new language features but also introduces mechanisms that help us keep our code less buggy and achieve higher code quality.
Recently I have introduced a couple of automatic TypeScript checks to a large-scale React project. It was a lot of work for me and my colleagues. In this article, I would like to share with you our thoughts on what we have learned during the experience. A list of the mentioned checks is:
noUnusedLocals,
noUnusedParameters,
noImplicitReturns,
noFallthroughCasesInSwitch,
noImplicitOverride,
allowUnreachableCode,
allowUnusedLabels,
noUncheckedIndexedAccess,
noPropertyAccessFromIndexSignature,
exactOptionalPropertyTypes.
This article is a continuation of a series of articles regarding automatic type checking rules for the TypeScript compiler. If you want to know more about it, I highly recommend reading the first part of the series: Bulletproof TypeScript - strict mode for Enterprise scale Applications
Dead code
It may seem that unused code is not that important because it cannot be responsible for creating a bug in your code. It is only an additional code that is not used, so why bother, right? Far from it. Code is something that is constantly read and improved. It is estimated that on average a programmer spends ten times more on reading code than on writing. Any additional code makes it harder to read and harder to understand. We should pay attention and don't leave any unused variables. When we go back to this code after a week, a month, or a year it would be way easier to understand. The meaning of the whole concept is more clear to wrap your mind around, when there are no unwanted, not working lines of code.
I remember that once I was working on a tree component. I have created the whole configuration for it upfront before creating essential features. After that, I started implementing those features. I created some of them and then I had to switch to a different task. After a couple of months, I wanted to use my tree component on a new page, so I configured it properly, but for some strange reason, some of the features didn't work. You can imagine my surprise when I remembered that I have never implemented them.
TypeScript compiler offers a couple of built-in checks that verify that there is no unused code:
- noUnusedLocals,
- noUnusedParameters,
- allowUnreachableCode,
- allowUnusedLabels.
noUnusedLocals
To my sadness, not a lot of programmers like to write unit tests. From what I saw while adding new TypeScript checks to my React project, lots of them write tests fast and they don't focus on quality. I often found code like:

When you come back to these tests after a while, you don't remember your intention for variable tree
or car
. You don't know if you are missing an assertion in your code or maybe it should be used in the isFruit
. It's simply better to don't leave unused code because it's impossible to figure out its intent after some time.
Enabling the noUnusedLocals
check will help you find these unused local variables:

noUnusedParameters
Defining API for a class or a feature is very important. You want other programmers to be able to use your code in the correct way. Specifying unused parameters may bring lots of confusion. Resolving the issue by removing parameters from the method may cause lots of breaking changes. It is smart to find these situations as soon as possible before improper usage of the API method will spread across the whole application.

The noUnusedParameters
check aids you to remove unused parameters from method declaration:

allowUnreachableCode
Maintaining large applications is expensive. The more code you have the more you need to spend time working on keeping it all together. From that perspective, it's better to get rid of dead code because it only slows you down in the development process. Less code also means a smaller bundle size, so better performance for your application.
Ts compiler allows you to easily find unreachable code. Enabling allowUnreachableCode
rule will allow you to spot dead code and remove it from the project:

The allowUnreachableCode
can help you not only get rid of dead code but also find bugs like in the example above where improper "if-else" usage changes the expected behavior of the isFruit function:

allowUnusedLabels
One of the most unknown and unused features of JavaScript is the label. Without going into details it works kind of like goto
used in other programming languages. There are a lot of articles on the topic of why you should never use goto
. Uncle Bob writes it best in his blog you can read about it.
Knowing all of that it is good to remove any labels from the code, but if you insist on having some, TypeScript compile will help you find the ones that are unused. That is what allowUnusedLabels
rule does:

Another interesting example of how this check might help is that sometimes developers tend to forget to type return:

The allowUnusedLabels
check will find this error:

Critical path
As your application grows bigger it's hard to keep track of all the possible scenarios. As programmers, we tend to see only happy paths. This is why QA is such an important person in a team.
TypeScript compilers offer a couple of checks that help you find less-used paths in your application. To be more precise:
- noImplicitReturns,
- noFallthroughCasesInSwitch.
noImplicitReturns
The next rule noImplicitReturns
may save lots of your time, as it verifies critical paths in your code. When you write a complicated method you may write it in a way that you forget to assure that every part of the method returns a value. This rule does it for you. Basically, it automatically verifies that every part of the function or method returns a declared value.

The compiler will easily find this error in the code:

noFallthroughCasesInSwitch
Similar to the previous rule this one relates to the switch
statement. It ensures that every case in a block of switch
code includes a return
or a break
:

Enabling the noFallthroughCasesInSwitch
check will allow you to find it:

From my experience, this situation happens rarely and that is the reason no one expects it.
Inheritance
I come from a Java background where inheritance is often commonly used. TypeScript is a language that implements all of the Object-Oriented Programming paradigms, so no wonder that programmers often chose inheritance as a design solution. That brings us to the situation where overriding base methods may create confusion:

As you can see class Dog
extends Animal
and overrides the makeSound
method. It is clear when you see the implementation of both classes. What if you see only the Dog
class?
You can make it more explicit with the new TypeScript feature the override
keyword which was introduced in language version 4.3. It makes code more readable because it shows the writer's intent which allows to better understand the code.

noImplicitOverride
With the new override keyword, TypeScript introduced a new noImplicitOverride
rule that helps you find places that require the use of it:

Index signatures
Long-lasting projects tend to have the legacy part of the codebase. In the case of the frontend, it is often written in JavaScript with a mix of some unsupported frameworks. To make the transition to TypeScript easier we could use the useful index signatures feature.

This feature offers a lot of flexibility, unfortunately, it may also bring some confusion and unwanted situations. That is why the TypeScript team introduced these two rules to the compiler that especially target the index signatures feature:
- noPropertyAccessFromIndexSignature,
- noUncheckedIndexedAccess.
noPropertyAccessFromIndexSignature
Loosening the requirements and introducing the "dot" notation brought situations in which programmers can make the mistake of accessing a property that doesn't exist. It could be a simple type in the code that is not easy to spot:

Turning on the noPropertyAccessFromIndexSignature
rule turns off the dot notation for the index signature of a given interface. If you want to access a property of an interface that is not explicitly declared and comes as an index signature you need to use bracket notation:

It is worth mentioning that the bracket notation introduces some security issues. To read more about it I suggest the article The Dangers of Square Bracket Notation".
noUncheckedIndexedAccess
The index signature allows you to exactly specify what is the type of the parameter. In other words, you don't know the property name but you know the type of it. It's better to assume that this unknown property may be present on an object or not. That is why we declare it as an index signature and not real property. Taking that into consideration it's better to expect that property could be undefined. The noUncheckedIndexedAccess
rule changes the behavior of the compiler so it verifies it for us:

exactOptionalPropertyTypes
The last presented rule is quite fresh and it was released in TypeScript version 4.4. Enabling it changes the way how compiler treats optional properties, lets's take a look at the example:

Property owner
is optional, so you can say that its type is string | undefined
:

So we could write:

The exactOptionalPropertyTypes
rule states that you cannot assign undefined
to an optional property because it is not its declared type. The type of property owner
from interface Dog
is string
, so you cannot assign undefined
to it:

Conclusion
TypeScript language is growing at a rapid speed. The team is working hard to bring new versions of the language as often as possible, which introduces many new features. They also don't forget about the tools that help us maintain our codebase. As projects grow bigger we put more and more effort into quality control. It brings the expectation that an application is less buggy and the code is much cleaner.
It may be hard to introduce new rules into the project, but trust me it is worth it. The overall code quality of your codebase will increase immensely. The best part that is you don't have to introduce new tools, everything is builtin into the TypeScript compiler. You can start using it by simply switching parameters in the tsconfig.json file.
I recommend starting a new project with as many rules turned on as possible. It will help you find bugs in the early stages of development and it will speed up the application growth in the long run. Nobody likes to spend nights debugging poorly written code.