tsconfig.json demystified (Part II)

Breaking down the various options and nuances to better understand what the Typescript compiler does under the hood

Alex Tzinov
Extra Credit-A Tech Blog by Guild
8 min readMay 15, 2020

--

This is the second part of a multi-part series exploring the various tsconfig.json options and when to use each one. See the first part exploring the Basic Options here. This part will cover Strict Type-Checking Options and Linter Options.

As a reminder, each option will list the Possible Values for the option (with the default option in bold), a single sentence TL;DR, and a When To Use that will help you decide when you might need the option.

Strict Type-Checking Options

strict:

Params (true | false)
TL;DR Set of all the strict options (strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization)
When To Use: When you want to enforce all the strict type checking rules to reduce runtime errors

This is an umbrella option that sets 4 separate strict options (each of which is covered in detail later below)

noImplicitAny:

Params (true | false)
TL;DR Error if TypeScript cannot infer the type of a variable/function
When To Use: When you want to enforce that all variables/functions have their types explicitly set.

By setting this to true, you are telling the TypeScript compiler to error if it can’t accurately infer the type of a function/variable. This forces you to explicitly declare types for all your variables, even if this means explicitly setting the any type.

The any type is dangerous in that you get no type guarantees and can have runtime errors if you don’t know what the intended type was.

noImplicitAny was set to false here so TypeScript assigned the type of any to the variable a. TypeScript is then unable to catch the error that will occur at runtime of trying to call a string method on a number

The any type is seen as so much of a disservice to the robustness of a code base that there is a TS Lint rule that completely disallows the any type from being assigned to a variable. The description of the rule makes the point that, “If you’re dealing with data of unknown or “any” types, you shouldn’t be accessing members of it” and suggests the alternatives of assigning it to the empty object type {} or to a generic <T>. While the task of converting a large legacy JS codebase over to TS and removing all instances of implicit and explicit any's can be an overwhelmingly daunting task, it is by leveraging the type-system and not steam rolling over it with any's that you get the most benefit out of TypeScript.

There is a caveat that is necessary to mention while discussing the importance of avoiding any's. Perhaps the only thing worse than using the any type is having incorrect types. A consumer of your library will trust your types to inform them on the correct way of using your library. If these types mislead the developer in using your library incorrectly, you have done an even bigger disservice.

strictNullChecks :

Possible Values (true | false)
TL;DR Error if you are accessing properties on a value that could be null or undefined
When To Use: When you want to have extra safe code that always handles the null/undefined edge case

This flag can help with perhaps the most common and insidious runtime error there is in JavaScript:

Cannot read property <property> of undefined

A quick search through Guild’s Rollbar logs (a JS error reporting tool) shows several such errors in our production app, many of which could possibly be prevented in the future by turning on strictNullChecks. What strictNullChecks does is prevent you from accessing fields/methods on any variable that could be null or undefined at runtime. In more technical terms, by default undefined and null are valid values for every type in TypeScript, whereas with strictNullChecks, “the null and undefined values are not in the domain of every type”. Here is a rather contrived but accurate example that portrays this:

Passing a number to a function that takes a string is clearly an error, but without strictNullChecks turned on, the last 2 calls are perfectly valid although they will error at runtime

strictNullChecks, arguably one of the most important flags in TypeScript, forces you to think about the undefined/null edge cases throughout your code. Pulling fields from an API response? Accessing attributes on a DOM element? Transforming a users input? Reading in browser storage? Any of these actions can result in undefined/null results and strictNullChecks will graciously force you to check for the null/undefined edge case before accessing properties from it.

strictNullChecks correctly telling us that since someMember might be undefined (if .find() fails to find a matching member), we cannot safely access the age property directly. Instead, we must check for the undefined case, and only if the object is undefined do we access the property. Otherwise we set a default.

Two operators closely related to strictNullChecks which are discussed in more detail in the footnotes are the null assertion operator¹ (denoted by !.) and optional chaining² (denoted by ?.).

strictFunctionTypes :

Possible Values (true | false)
TL;DR Error if you have functions that are being reassigned to functions with looser parameter types.
When To Use: You want stricter type checking on your functions.

We declare a type for a function that will take either a number or a string. With strictFunctionTypes turned on, we cannot “open up” the parameter types of our square and upperCase functions by retyping them to a correct but looser type

This setting involves some fairly theoretical and technical topics of language theory called covariance and contravariance. In the interest of keeping this article easy to understand, I have included a gist below that serves as a dive into the kinds of problems that strictFunctionTypes will catch. To see an in-depth explanation, see this article.

strictBindCallApply :

Possible Values (true | false)
TL;DR Enforces strict types on all call , apply, and bind methods
When To Use: You want stricter type checking when using call, apply, and bind

Without strictBindCallApply turned on, we can see that we lose all type information from our upperCase function when using it with call, apply, or bind (as well as type safety; the arguments in all of these calls are wrong)

strictPropertyInitialization:

Possible Values (true | false)
TL;DR Requires proper initialization of class instance variables (set in constructor, given a default value, or typed as possibly undefined)
When To Use:
When you want stricter typing around instance variables in classes

When set to true, TypeScript will raise an error when a class property was declared but not set in the constructor.

with strictPropertyInitialization set to true, we are unable to declare a property on a class. birthMonth is allowed because we state it could possibly be undefined, birthYear is assigned a hard-coded value, and name and age get set in the constructor, but birthDay violates strictPropertyInitialization

noImplicitThis:

Possible Values (true | false)
TL;DR Prevents the use of this when its value is not
When To Use:
When you want to ensure you are properly using the this keyword, especially when using fat arrow functions / anonymous functions.

Functions in JavaScript have a this value that is dependent on a number of factors.

If they are called on an object (myAnimalObject.bark()), they will have a this value equal to myAnimalObject, the calling object.

If they have a this value assigned using .call()|.bind()|.apply() , they will have a this value equal to what is passed in. (myAnimalObject.bark.call(someOtherAnimalObject))

If they are called as an anonymous function, with no calling context, this is undefined when using strict mode and equal to Global/window if not in strict mode.

What noImplicitThis does is prevent you from using this if it falls into this last category of being undefined / window

With setNumberOfWheels, because we are using the function keyword for the returned anonymous function instead of the fat arrow (a fat arrow function => does not have its own ‘this’ but has the same ‘this’ value of the enclosing execution context, in this case Vehicle), TypeScript is warning us that the value of `this` is implicit

alwaysStrict:

Possible Values (true | false)
TL;DR Forces all your code to run in JavaScript strict mode
When To Use:
When you want to run all your code in strict mode which can help with JavaScript robustness and error handling

See for a more in-depth explanation on strict mode here

Linter Options

noUnusedLocals:
Will error if you have any declared local variables that are not being used.

noUnusedParameters:
Will error if you have passed in parameters to functions that you are not using

noImplicitReturns:
Functions are checked across all possible code paths and each must explicitly return a value

In this case, we are implicitly returning n in the case that our number is odd, and noImplicitReturns will warn you and force to be explicit about returns

noFallthroughCasesInSwitch:
Ensures that every case in a particular switch statement ends in a break or return.

In this case, because we don’t have a break in our “hi” case, it will fall through to the default case and end up printing twice

Footnotes

[1] The Null Assertion Operator (!.)
When dealing with strictNullChecks which force you to handle the null/undefined edge case, you might have come across an operator that looks like this. const someMemberAge = someMember!.age. What you’ll notice this operator does on first glance is remove the error and make your code compile. How nice. Will this change whether or not someMember is defined at runtime? Absolutely not:

Incorrectly suppressing the strictNullChecks error using the ! operator
…and the expected runtime error.

The Null Assertion operator is for situations when the developer can guarantee that a variable is going to be defined despite the TypeScript compiler being unable to do so. These situations are rare but they do exist (eg: React ref for elements). In these circumstances, it is important to note the knowledge burden that is being placed on the supporting developers of the code (which can include you in the future). By using the ! operator, you are telling TypeScript, “I have more context, I know more than you, and I am telling you with 100% certainty this value can never be null/undefined”.

It is easier (and safer) in my opinion to handle the null/undefined case rather than accrue a collection of exceptions over time, all of which require developer thought to ensure a runtime error won’t occur.

[2] Optional Chaining(?.)
Often times the solution to handling strictNullChecks without suppressing the warning using the ! operator is to do a “existential check” on the object you are trying to access fields from. someMember && someMember.age will either return the age if someMember exists or it will gracefully return undefined/null without throwing an error. Because the statement will “short-circuit” if someMember is undefined/null and not evaluate someMember.age , TypeScript is happy even with strictNullChecks enabled. If you had to reach deeply nested properties, your code would look like this:

someMember && someMember.bestFriend && someMember.bestFriend.address

and so and so forth, repeating existential checks for nested objects to ensure they if any of the nested objects ended up undefined/null at runtime, you wouldn’t blow up by trying to access them. Optional Chaining gives us a nicer syntax for this:

someMember?.bestFriend?.address

We either end up returning someMember.bestFriend.address if all nested objects are defined or we terminate early and return undefined . A note: Optional Chaining will not short circuit on valid but “falsey” data like 0 or “”

--

--