tsconfig.json demystified (Part II)
Breaking down the various options and nuances to better understand what the Typescript compiler does under the hood
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.
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:
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.
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.
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
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.
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
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
noFallthroughCasesInSwitch
:
Ensures that every case in a particular switch statement ends in a break or return.
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:
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 “”