How TypeScript’s Strict Mode Actually Fixes TypeScript
There is no reason not to turn it on by default
Strict mode is actually a combination of six other flags (as of TypeScript 3.8):
noImplicitAny
strictNullChecks
(since 2.0)noImplicitThis
(since 2.0)strictFunctionTypes
(since 2.6)strictPropertyInitialization
(since 2.7)strictBindCallApply
(since 3.2)
Each option can be enabled or disabled separately. Some of them make TypeScript’s type checking better and some help make your code more readable and less error-prone.
You can enable strict mode in your tsconfig.json
:
You can disable any option that you don’t like from the strict family in the compileOptions
as well (e.g. "noImplicitAny": false
).
I think the most important flags are noImplicitAny
and strictNullChecks
. These two will really improve the type checking and readability of your code.
Let’s take a look at each flag.
noImplicitAny
Without turning noImplicitAny
on, you are only mostly using TypeScript because now you have parts of your code that are of type any
without you even noticing.
Because the type any
basically disables type check, you really shouldn’t have it unless you don’t have a choice. The problem is that it’s really easy to have any
in your code by mistake, so by using noImplicitAny
, you will now only have any
where you explicitly use it.
Let’s see how easy it is to disable type checking by mistake:
In this example, we define a function (fn
) that just returns what it gets. What we can easily miss is that the return type of this function is actually any
.
You can see that copyOfNum
, which is just the same number as num
, is now of type any
and we can do all sorts of wrong things with it.
If we turn noImplicitAny
on, we won’t be able to define a function like fn
that returns any
implicitly and would have to define it correctly instead, resulting in copyOfNum
not losing its type:
Another common way we put any
in our code without noticing is when importing an external module:
Here, we just wanted to add 1 + 1 using lodash but got a result of type any
because we didn’t have the types of lodash installed. This can be solved by installing @types/lodash
. In case we are using an external module with no available types, we can write them ourselves using declare module ‘lodash’ { /* types goes here */}
.
If we are OK with this module being of type any
or do not have the capacity right now to fix it, we can signal it to the compiler by declaring the module with no types:
declare module ‘lodash’;
strictNullChecks
The most common JS runtime error is probably Uncaught TypeError: Cannot read property ‘foo’ of undefined.
This is caused when trying to access a property or call a method on an undefined
object.
Luckily, TypeScript can help.
By default, null
and undefined
are assignable to all other types, while your code actually has many types that can never be null
or undefined
, making their types wrong by default.
After enabling strictNullChecks
, the only types that can be null
or undefined
are the ones explicitly marked as so. This means you wouldn’t be able to initialize a variable without a value. For example:
What do you get from all this extra work? Now that your types are more specific, TypeScript will be able to analyze your code flow and find potential bugs:
Sometimes you have to opt out of this feature. Maybe some external types are wrong, but you have no way to fix them or you want to gradually introduce this flag. You can always disable strictNullChecks
by using the !
symbol.
Any property access that has the !
symbol will be ignored in this check:
Only if you enable strictNullChecks
will you be able to enable the next flag.
strictPropertyInitialization
With strictPropertyInitialization
, TypeScript will throw an error unless all class properties are initialized in the constructor or by a property initializer. This is used to help prevent unintentional access to undefined
props in your code:
Sometimes you can’t initialize in the class creation. For example, maybe you use an external service to fetch data. In these cases, you can ignore strictPropertyInitialization
by using the !
symbol just like you used it for strictNullChecks
:
strictFunctionTypes
In TypeScript, argument types are bivariant (both covariant and contravariant), which is unsound (although you can now fix this in TypeScript 2.6 with --strictFunctionTypes
or --strict
).
Let’s look at a simple example to understand what it means:
In this example, we call forEach
with another function that gets an HTMLElement
even though querySelectorAll
returns a list of Element
(which HTMLElement
extends). You can see that we access the element’s offsetHeight
, a property that doesn’t exist on Element
but only on HTMLElement
.
Without strictFunctionTypes
, this code will compile, but its types are wrong and can lead to potential bugs. If we turnstrictFunctionTypes
on, we will get an error:
Type ‘Element’ is missing the following properties from type ‘HTMLElement’ …
This basically means that we can’t decide that element
is HTMLElement
instead of Element
because then we can use properties that only HTMLElement
has inside our function, which is wrong.
strictBindCallApply
With the strictBindCallApply
flag, the bind
, call
, and apply
methods are strongly typed. Without this flag, you can mistakenly use these three functions with arguments that don’t match the function they are used on:
noImplicitThis
This acts the same way as noImplicitAny
, but for the this
type. If you have a function that uses this
, but the compiler can’t infer what the type of this
is from the code, you will have an error. This can help catch bugs in compile time instead of in runtime. For example:
In this example, we get a runtime error:
Uncaught TypeError: Cannot read property ‘x’ of undefined.
We get an error because we called isEqual
with the wrong this
parameter (we lost the this
binding when we assigned the function to another variable in line 11).
If we had the noImplicitThis
flag on, instead of a runtime error, we would get a compile-time error on line 6: ‘this’ implicitly has type ‘any’
. We can fix this error by writing the this
type explicitly, which will force us to fix the bug:
Conclusion
I hope you now understand strict mode and that you should use it in your code because the benefits are greater than the price — at least for new projects that don’t require migration.
For existing projects, consider adding one strict flag at a time, making the migration more gradual.