What’s in the tsconfig.json
?
Let’s look closely at what we just did, and make sure we understand all of the parts that make up the whole
In my tsconfig.json
what exactly is “strict”?
The source of truth is here, and it’s important to know that this is a moving target
noImplicitAny
- “Default to explicit” instead of “default to loose”
- This is not in any way restrictive, it only requires that we be explicit about
any
noImplicitThis
- There are certain places where
this
is important and non-inferrable
Example: addEventListener
tsTry
constmy_element =document .createElement ('button')my_element .addEventListener ('click', function (e ) {thisconsole .log (this.className )// logs `true`console .log (e .currentTarget === this)})
If we wanted to make it an independent function declaration and had noImplicitThis
enabled, we’d be asked to provide a proper this
type annotation
tsTry
functionclickListener (e :MouseEvent ) {'this' implicitly has type 'any' because it does not have a type annotation.2683'this' implicitly has type 'any' because it does not have a type annotation.console .log (this .className )'this' implicitly has type 'any' because it does not have a type annotation.2683'this' implicitly has type 'any' because it does not have a type annotation.console .log (e .currentTarget ===this )}
tsTry
functionclickListener (this :HTMLButtonElement ,e :MouseEvent ) {console .log (this.className )console .log (e .currentTarget === this)}
alwaysStrict
- JS “use strict”
- necessary for modern JS language features
strictBindCallApply
- Bind, call, apply used to return very loosely-typed functions.
- No good reasons I’m aware of to disable this
strictNullChecks
- Without this enabled, all types allow
null
values - Leaving this disabled makes truthy/falsy type guards much less useful
- Operating without
strictNullChecks
is asking for runtime errors that could otherwise be caught at build time
tsTry
// Without `strictNullChecks` enabledclassAnimal {run () {}}letanimal = newAnimal ()animal = null // type-checks, but do we really want it to?
strictFunctionTypes
Some common-sense loopholes around matching function arguments during type-checking function values
tsTry
abstract classAnimal {public abstract readonlylegs : number}classCat extendsAnimal {legs = 4purr () {console .log ('purr')}}classDog extendsAnimal {legs = 4bark () {console .log ('arf')}}letanimalFn !: (x :Animal ) => voidletdogFn !: (x :Dog ) => voidletcatFn !: (x :Cat ) => voidType '(x: Dog) => void' is not assignable to type '(x: Animal) => void'. Types of parameters 'x' and 'x' are incompatible. Property 'bark' is missing in type 'Animal' but required in type 'Dog'.2322Type '(x: Dog) => void' is not assignable to type '(x: Animal) => void'. Types of parameters 'x' and 'x' are incompatible. Property 'bark' is missing in type 'Animal' but required in type 'Dog'.= animalFn dogFn // Error with --strictFunctionTypesdogFn =animalFn // Always okType '(x: Cat) => void' is not assignable to type '(x: Dog) => void'. Types of parameters 'x' and 'x' are incompatible. Property 'purr' is missing in type 'Dog' but required in type 'Cat'.2322Type '(x: Cat) => void' is not assignable to type '(x: Dog) => void'. Types of parameters 'x' and 'x' are incompatible. Property 'purr' is missing in type 'Dog' but required in type 'Cat'.= dogFn catFn // Always error
If you took my Intermediate TypeScript course, you may recognize this as detecting when functions are contravariant over their argument types, and preventing a problematic assignment.
strictPropertyInitialization
Holds you to your promises around class fields really being “always there” vs. “sometimes undefined”
tsTry
classCar {Property 'numDoors' has no initializer and is not definitely assigned in the constructor.2564Property 'numDoors' has no initializer and is not definitely assigned in the constructor.: number numDoors }
Even more strict
noUnusedLocals
- Busts you on unused local variables
- Better to have TS detect this rather than a linter
noUnusedParameters
Function arguments you don’t use need to be prefixed with _
tsTry
const'resolve' is declared but its value is never read.6133'resolve' is declared but its value is never read.p = newPromise ((, resolve reject ) => {reject ("boom")});
diff
- const p = new Promise((resolve, reject) => {+ const p = new Promise((_resolve, reject) => {
tsTry
constp = newPromise ((_resolve ,reject ) => {reject ("boom")});
noImplicitReturns
If any code paths return something explicitly, all code paths must return something explicitly. I’m a big fan of explicitly typing function boundaries, so I love this compiler setting
noFallthroughCasesInSwitch
I’m ok with this one as being disabled, as I find case fall-throughs to be useful, important and easy (enough) to notice while reading code
types
- Instead of pulling in all @types/* packages, specify exactly what should be available
- NOTE: this is nuanced, and only affects global scope (i.e., window, process) and auto-import.
- Why I care: I don’t want types used exclusively in things like tests to be quite so readily available for accidental use in “app code”
stripInternal
(most important for libraries)
- Sometimes you need type information to only be available within a codebase.
@internal
JSdoc tag surgically strips out type information for respective symbols
exactOptionalPropertyTypes
In essence, I think of this one as establishing some very reasonable rule of appropriate distinction between null
and undefined
If we have a type like this
tsTry
interfaceVideoPlayerPreferences {volume ?: number // 0 - 100, initially unset}
and enable exactOptionalPropertyTypes
, we’ll be stopped from doing things like this
tsTry
constprefs :VideoPlayerPreferences = {volume : 50}Type 'undefined' is not assignable to type 'number' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.2412Type 'undefined' is not assignable to type 'number' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.= prefs .volume undefined // Bad practicedeleteprefs .volume // Good practice
What we’re seeing here is that we’re being stopped from explicitly setting an optional property to undefined
. This is a good thing to prevent, because it causes some strangeness around things you’d expect to behave a certain way with undefined properties
tsTry
prefs .hasOwnProperty ('volume') // returns TRUEObject .keys (prefs ) // includes "volume"for (letkey inprefs ) {} // will iterate over "volume" key
The fact is, we do have a property on prefs
, and it has a value. We’ve just made that value undefined
.
It’s much more appropriate to use null
for these cases, since it’s an explicitly provided value that can’t be confused with the thing we get when we ask for properties that don’t exist.
noUncheckedIndexedAccess
Sometimes we can have object types which have some known properties as well as index signatures
tsTry
interfacePhoneBookEntry {name : stringhome_phone : stringcell_phone : string[k : string]: string}
This rule ensures that undeclared keys have the undefined
type added to them, to represent the possibility that no value is present
tsTry
functioncallback (phoneBook :PhoneBookEntry ) {phoneBook .name phoneBook .office_phone // would have been `string`}
noPropertyAccessFromIndexSignature
This rule draws a distinction between .foo
syntax (to be used for known property access) and ["foo"]
syntax (to be used for index signatures). Using the same example from above
tsTry
functioncallback (phoneBook :PhoneBookEntry ) {phoneBook .name Property 'office_phone' comes from an index signature, so it must be accessed with ['office_phone'].4111Property 'office_phone' comes from an index signature, so it must be accessed with ['office_phone'].phoneBook .// would have been `string` office_phone }
noImplicitOverride
The override
keyword prevents incomplete refactors and other kinds of errors relating to object-oriented inheritance
tsTry
classAnimal {walk () {}}classDog extendsAnimal {overridewalk () {}}
if we make this change to Animal
diff
- walk() {}+ run() {}
We’ll be appropriately alerted
tsTry
classAnimal {run () {}}classDog extendsAnimal {overrideThis member cannot have an 'override' modifier because it is not declared in the base class 'Animal'.4113This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'.() {} walk }
So this override
keyword is quite valuable — we just need to make sure we put it in place where appropriate. noImplicitOverride
helps guide us to do just that.
tsTry
classAnimal {walk () {}}classDog extendsAnimal {This member must have an 'override' modifier because it overrides a member in the base class 'Animal'.4114This member must have an 'override' modifier because it overrides a member in the base class 'Animal'.() {} walk }
Don’t go viral
There are some compiler options that I really dislike when used in libraries, because they have a high probability of “infecting” any consumer and depriving them from making choices about their own codebase
allowSyntheticDefaultImports
Allows you to import CommonJS modules as if they’re ES modules with a default export
esModuleInterop
Adds some runtime support for CJS/ESM interop, and enables allowSyntheticDefaultImports
skipDefaultLibCheck
This effectively ignores potential breaking changes that stem from your node_modules types mixing with your own types. Particularly if you’re building a library, you need to know that if you “hide” this problem they’ll still “feel” it (and probably need to “skip” too)
useUnknownInCatchVariables
Caught throwables in a catch block will be typed as unknown
instead of any
, promoting better error-handling hygiene
tsTry
try {}catch (err ) { }
But sometimes we need these, right?
I have never found a good reason to enable these options in well-structured TS code.
allowSyntheticDefaultImports
and esModuleInterop
aim to allow patterns like
ts
import fs from 'fs'
in situations where fs
doesn’t actually expose an ES module export default
. It exports a namespace of filesystem-related functions. Thankfully, even with these flags both disabled, we can still use a namespace import:
ts
import * as fs from 'fs'
Now there are rare situations where some CommonJS code exports a single non-namespace thing in a way like
ts
module.exports = function add(a, b) {return a + b}
add is definitely not a namespace, and
ts
import * as add from './calculator'
WILL NOT WORK. There’s a TS-specific pattern that will work though — it’s a little weird, but it doesn’t require turning any compiler options on
ts
import add = require('./calculator')
Is Mike asking me to take on tech debt?
You may be thinking “these don’t look like ES modules”, and “won’t the TS team standardize on ES modules later?”
The answer is: yes, but you should think about this the same way that you think about a “legacy” version of Node.js that you need to support
- You may not wish to break consumers yet
- Apps should be the first to adopt new things, followed by libraries that are more conservative
TS modules predate ES modules, but there’s tons of code out there that already uses TS module stuff, and this is one of the most easy to codemod kinds of “tech debt” to incur.