Bulletproof TypeScript - code quality rules beyond strict mode

Picture of a work place.
Photo by Karl Pawlowicz on Unsplash

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:

Example code showing unused local variables
Example code showing unused local variables

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:

TypeScript compiler shows error for unused variables
TypeScript compiler shows error for unused 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.

Example code presenting unused parameter
Example code presenting unused parameter

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

The compiler shows unused parameter
The compiler shows unused parameter

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:

Example code showing unreachable code
Example code showing unreachable code

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:

TS compiler finding unreachable code
TS compiler finding unreachable code

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:

Example code showing an unused label
Example code showing an unused label

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

Example showing missing return statement
Example showing missing return statement

The allowUnusedLabels check will find this error:

Example showing missing return statement
Example showing missing return statement

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.

Example code showing function that doesn't always return a value
Example code showing function that doesn't always return a value

The compiler will easily find this error in the code:

Compiler presenting error from the rule check
Compiler presenting error from the rule check

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:

Example code showing potential error in switch case
Example code showing potential error in switch case

Enabling the noFallthroughCasesInSwitch check will allow you to find it:

TS compiler finding issue in the switch case
TS compiler finding issue in the switch case

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:

Simple inheritance example
Simple inheritance example

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.

Example with override usage
Example with override usage

noImplicitOverride

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

Example code with missing override
Example code with missing override

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.

Example of Index signature
Example of Index signature

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:

Example code presenting typo that is an unwanted side effect of dot notation
Example code presenting typo that is an unwanted side effect of dot notation

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:

TS Compiler catching dot notation mistake
TS Compiler catching dot notation mistake

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:

Code example presenting the noUncheckedIndexedAccess check
Code example presenting the noUncheckedIndexedAccess check

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:

Simple interface with optional property
Simple interface with optional property

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

Simple interface with optional property with a changed type definition
Simple interface with optional property with a changed type definition

So we could write:

Example of assigning undefined to a property
Example of assigning undefined to a property

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:

TS Compiler found improper assigning to a property
TS Compiler found improper assigning to a property

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.

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.