Controlling Type Checking Strictness in TypeScript
In this post, we will explore the compiler options that control the strictness of the type checking in TypeScript.
Understanding TypeScript strict mode
There is a TypeScript compiler option called strict
. This turns on a set of type checking rules and is referred to as strict mode. This is separate from JavaScript’s strict mode.
When creating a new TypeScript project, it is recommended to have strict mode on so that code benefits from the most stringent type checking from the start of its life. However, strict mode may not be feasible when migrating a JavaScript codebase to TypeScript because of the number of type errors raised.
In addition to the strict
option, each type checking rule can be controlled via a specific compiler option. This gives fine-grain control on the type checker’s strictness.
Understanding noImplicitAny
The noImplicitAny
compiler option will raise errors in expressions and declarations with an implied any
type. This option is true
by default when the strict
option is true
.
The greet
method below is currently violating this rule because the prefix
parameter is inferred to have the any
type:
public greet(prefix) {
...
}
We could set the noImplicitAny
compiler option in tsconfig.json
to false
to remove the type error:
{
"compilerOptions": {
...
"noImplicitAny": false
},
....
}
To properly resolve the error, we would give the prefix
parameter a type annotation:
public greet(prefix: string) {
...
}
Understanding noImplicitThis
The noImplicitThis
compiler option raises an error on expressions that reference this
with an implied any
type. This option is true
by default when the strict
option is true
.
The goodbyeFunction
method below is voilating this rule where the name
property within this
is accessed:
class Person {
constructor(private name: string) {}
goodbyeFunction() {
return function() {
console.log(`Goodbye ${this.name}`);
};
}
}
TypeScript infers this
to be of type any
and so raises a type error.
We could set the noImplicitThis
compiler option in tsconfig.json
to false
to remove the type error:
{
"compilerOptions": {
...
"noImplicitThis": false
},
....
}
There are two ways to resolve this error correctly.
The first method of resolving the type error is to let TypeScript know the type of this
in the function signature:
goodbyeFunction() {
return function (this: Person) {
...
};
}
The second method of resolving the type error is to use an arrow function:
goodbyeFunction() {
return () => {
...
};
}
Nice!
Understanding strictBindCallApply
The strictBindCallApply
compiler option enables stricter checking of the bind
, call
, and apply
methods on functions. This option is true
by default when the strict
option is true
.
The call to person.calculatePrice
below is violating this rule because calculatePrice
requires two arguments to be passed in. The function type is (discountCode: string, price: number) => number
.
const discountedPrice = person.calculatePrice.apply(
undefined,
["FREE"]
);
We could set the strictBindCallApply
compiler option in tsconfig.json
to false
to remove the type error:
{
"compilerOptions": {
...
"strictBindCallApply": false
},
....
}
To properly resolve the error we should pass the second parameter in the function call:
const discountedPrice = person.calculatePrice.apply(
undefined,
["FREE", 100]
);
Understanding strictNullChecks
The strictNullChecks
compiler option excludes the null
and undefined
values from the domain of every type. This option is true
by default when the strict
option is true
.
The assignment of person
to null
in the code below is violating this rule because null
is not within the Person
type.
let person = new Person();
...
person = null;
We could set the strictNullChecks
compiler option in tsconfig.json
to false
to remove the type error:
{
"compilerOptions": {
...
"strictNullChecks": false
},
....
}
To better resolve this error we could add null
to the type of person
in a union type:
let person: Person | null = new Person();
...
person = null;
Understanding strictFunctionTypes
The strictFunctionTypes
disables bivariant parameter checking for function types. This option is true
by default when the strict
option is true
.
The getDiscountCode
function below contains a type error because the info
parameter contains a token
property that is not part of APIRequestHandler
:
type APIRequestHandler<T> = (info: { path: string }) => Promise<T>;
type APIRequestInfo = { path: string };
const getDiscountCode: APIRequestHandler<string> = async (
info: APIRequestInfo & { token: string }
) => { ... }
We could set the strictFunctionTypes
compiler option in tsconfig.json
to false
to remove the type error:
{
"compilerOptions": {
...
"strictFunctionTypes": false
},
....
}
Resolving this type error isn’t straight forward. The getDiscountCode
clearly requires the token property. We could change the APIRequestHandler
type, but that might impact other code. A solution is to remove the type annotation from getDiscountCode
and let TypeScript infer its type.
Understanding strictPropertyInitialization
The strictPropertyInitialization
compiler option ensures non-undefined class properties are initialized in the constructor. This option requires strictNullChecks
to be enabled to take effect. strictPropertyInitialization
is true
by default when the strict
option is true
.
The name
property within the Person
type below is currently violating this rule.
class Person {
public name: string;
}
We could set the strictPropertyInitialization
compiler option in tsconfig.json
to false
to remove the type error:
{
"compilerOptions": {
...
"strictPropertyInitialization": false
},
....
}
We could assign a default value to name
to properly resolve the error:
class Person {
public name: string = "";
}
Understanding alwaysStrict
The alwaysStrict
compiler option ensures JavaScript strict mode is used type checking process. It also determines whether "use strict"
is emitted in JavaScript. This option is true
by default when the strict
option is true
.
When alwaysStrict
is on, "use strict"
will be at the top of the transpiled JavaScript files:
"use strict";
class Person ...
In JavaScript strict mode, you can’t name a variable “arguments” like below:
let arguments;
We could set the alwaysStrict
compiler option in tsconfig.json
to false
to remove the type error:
{
"compilerOptions": {
...
"alwaysStrict": false
},
....
}
It is, however, advisable to correctly resolve JavaScript strict mode errors. In this case, we’d renamed the variable to something else:
let args;
Summary
TypeScript has a range of options for controlling how strict the type checking is:
strict
- Sets maximum strictness, setting all the flags below totrue
.noImplicitAny
- Raises an error on expressions and declarations with an inferred type ofany
.noImplicitThis
- Raises an error onthis
expressions with an inferred type ofany
.strictBindCallApply
- Enables strict checking of thebind
,call
, andapply
methods on functions.strictNullChecks
- Means thatnull
andundefined
values are not valid values in types.strictFunctionTypes
- Disables bivariant parameter checking for function types.strictPropertyInitialization
- Ensures class properties are assigned a default value or initialized in the constructor.alwaysStrict
- Parses the code in JavaScript strict mode and emits “use strict” in the transpiled code.
For new projects, it is recommended to set strict
to true
.
For JavaScript migration projects, strict
can be set to true
with the other flags to false
if the corresponding rule raises lots of type errors. A goal could be to resolve the type errors and set all the strict
flags to true
over time.
If you to learn more about TypeScript, you may find my free TypeScript course useful: