How to Actually Improve Type Safety with the TypeScript Strict Flags

Last summer a team of engineers converted our front-end code from Closure-annotated JavaScript to TypeScript, you can read about it here. It has been a long journey to take that converted code that was smattered with anys and turn it into idiomatic and type-safe TypeScript. The strict compiler flags have been a primary method for making these improvements. Read on to find out why enabling the strict TypeScript compiler flags “the hard way” is worth it in the long term for improving type safety.

The strict flags

The TypeScript compiler currently has eight* strict compiler flags that can be used as tools to improve code quality and, to varying degrees, increase the soundness of TypeScript’s type system:

  1. noImplicitThis
  2. noImplicitReturns
  3. noUnusedLocals
  4. noUnusedParameters
  5. noImplicitAny
  6. strictNullChecks
  7. strictFunctionTypes
  8. strictPropertyInitialization

* alwaysStrict is also included in the strict flag, but it is different than the rest in that it notably modifies the emit.

I, and anyone else that has worked in a TypeScript codebase, can vouch for the value of the strict flags, but the journey to enabling the flags on an existing codebase can be taken in a few different ways.

The easy way

Both TSLint and Tsetse are linters created for TypeScript that include automated fixers for some rules. For example, with noImplicitAny for

function example(val) {

the compiler will output an error like

Error at srcfile.ts:11:18: Parameter 'val' implicitly has an 'any' type.

And the error can be fixed by inserting “: any” beginning at column 18 (from the error) + “val”.length = 21.

function example(val: any) {

This is easy for a program to do automatically—all errors with noImplicitAny are easily resolved, and, by the end of the day, you can have your million+ line codebase fully compliant with a strict compiler flag.

The hard way

Another way to enable a strict compiler flag is to go through each error message from the compiler, one at a time, resolving each error.

If you are like me, you probably just laughed at that idea, but allow me to persuade you that the benefits of this method are worth the costs.

Ask yourself what the reasons are for enabling the strict flags. The answer is obvious: you want to create a sound, type-safe codebase. Adding explicit anys to every location that an error is found will do nothing for improving type safety now, and forcing future code to comply will do little good compared to the harm caused by inadvertently overwriting valid type information with the contagious any.

Consider this simple code sample.

declare function getInputs(): any;
getInputs().forEach((input) => console.log(input.property.value));

noImplicitAny will throw an error that input is implicitly any. If we were a robot, we would naively mute that error by writing (input: any) => ..., happily enable noImplicitAny, and rejoice that our code is now held to a higher standard. But now imagine another engineer updates the API of getInputs, and so now we have:

declare function getInputs(): string[];
getInputs().forEach((input: any) => console.log(input.property.value));

This code—unfortunately—also compiles but will throw an error at runtime. Had input not been annotated with an any, this code would have failed to compile, and the engineer changing the API would have been able to resolve the issue. However, by explicitly annotating the type, that opportunity was lost. By choosing the easy path toward the strict flags, it is possible to lose the benefits that you hoped to gain from enabling them.

If a human looked at this same code, they could assume that getInputs returns an array and change the type to be at least any[], if not a more specific type.

The best of both

It took approximately 400 engineer-hours to enable just the noImplicitAny flag by hand for our codebase. This process was spread over 150 days from start to finish. That is a long time in terms of code. Am I suggesting that you should simply go without the benefits of the flags until someone has the bandwidth to solve what could be literally tens of thousands of errors for a large legacy codebase? No, there is a way to get incremental benefits.

By splitting your code into separate TypeScript projects (with individual tsconfig.jsons), you can enable the strict flag for one project at time, locking in incremental type safety while other projects are being worked on.

While it is tempting to lock in type safety by programmatically making code compliant with the strict TypeScript compiler flags, doing so has costs that shouldn’t be lightly ignored. Using the compiler errors as an asset to direct attention to improving types rather than ignoring them will not only prevent future types from being absorbed by an any but will improve the type safety of your code, which is probably the value you hoped to get from using TypeScript in the first place.

3 Comments

  1. > compared to the harm caused by inadvertently overwriting valid type information with the contagious any.

    I didn’t get this part. Could you please elaborate on that? What do you mean?
    if the type was already inferred as ‘any’, changing it from implicit to explicit shouldn’t do any harm.

  2. Okay I should finished reading the article before asking my question, because it explains this further in the text. Great advice, thanks for the article.

Your email address will not be published.