Non-Nullable Types in TypeScript
The release of TypeScript 2.0 shipped with plenty of new features. In this post, we'll be looking at non-nullable types, a fundamental improvement to the type system that helps prevent an entire category of nullability errors at compile-time.
#The null
and undefined
Values
Prior to TypeScript 2.0, the type checker considered null
and undefined
to be valid values of every type. Basically, null
and undefined
could be assigned to anything. That included primitive types such as strings, numbers, and booleans:
let name: string;
name = "Marius"; // OK
name = null; // OK
name = undefined; // OK
let age: number;
age = 24; // OK
age = null; // OK
age = undefined; // OK
let isMarried: boolean;
isMarried = true; // OK
isMarried = false; // OK
isMarried = null; // OK
isMarried = undefined; // OK
Let's take the number
type as an example. Its domain not only includes all IEEE 754 floating point numbers, but the two special values null
and undefined
as well:
The same was true for objects, array, and function types. There was no way to express via the type system that a specific variable was meant to be non-nullable. Luckily, TypeScript 2.0 fixes that problem.
#Strict Null Checking
TypeScript 2.0 adds support for non-nullable types. There's a new strict null checking mode that you can opt into by providing the --strictNullChecks
flag on the command line. Alternatively, you can enable the strictNullChecks
compiler option within your project's tsconfig.json file:
{
"compilerOptions": {
"strictNullChecks": true
// ...
}
}
In strict null checking mode, null
and undefined
are no longer assignable to every type. Both null
and undefined
now have their own types, each with only one value:
If we compile our previous examples with strict null checks enabled, attempting to assign null
or undefined
to any of the variables results in a type error:
// Compiled with --strictNullChecks
let name: string;
name = "Marius"; // OK
name = null; // Error
name = undefined; // Error
let age: number;
age = 24; // OK
age = null; // Error
age = undefined; // Error
let isMarried: boolean;
isMarried = true; // OK
isMarried = false; // OK
isMarried = null; // Error
isMarried = undefined; // Error
So how do we make a variable nullable in TypeScript 2.0?
#Modeling Nullability with Union Types
Since types are non-nullable by default when strict null checking is enabled, we need to explicitly opt into nullability and tell the type checker which variables we want to be nullable. We do this by constructing a union type containing the null
or undefined
types:
let name: string | null;
name = "Marius"; // OK
name = null; // OK
name = undefined; // Error
Note that undefined
is not a valid value for the name
variable since the union type doesn't contain the undefined
type.
A big advantage of this nullability approach is that it becomes evident and self-documenting which members of a type are nullable. Take this simple User
type as an example:
type User = {
firstName: string;
lastName: string | undefined;
};
let jane: User = { firstName: "Jane", lastName: undefined };
let john: User = { firstName: "John", lastName: "Doe" };
We can make the lastName
property optional by appending a ?
to its name, which allows us to omit the definition of the lastName
property entirely. In addition, the undefined
type is automatically added to the union type. Therefore, all of the following assignments are type-correct:
type User = {
firstName: string;
lastName?: string;
};
// We can assign a string to the "lastName" property
let john: User = { firstName: "John", lastName: "Doe" };
// ... or we can explicitly assign the value undefined
let jane: User = { firstName: "Jane", lastName: undefined };
// ... or we can not define the property at all
let jake: User = { firstName: "Jake" };
#Property Access with Nullable Types
If an object is of a type that includes null
or undefined
, accessing any property produces a compile-time error:
function getLength(s: string | null) {
// Error: Object is possibly 'null'.
return s.length;
}
Before accessing a property, you need to use a type guard to check whether the property access on the given object is safe:
function getLength(s: string | null) {
if (s === null) {
return 0;
}
return s.length;
}
TypeScript understands JavaScript's truthiness semantics and supports type guards in conditional expressions, so this approach works fine as well:
function getLength(s: string | null) {
return s ? s.length : 0;
}
#Function Invocations with Nullable Types
If you attempt to call a function that is of a type that includes null
or undefined
, a compile-time error is produced. The callback
parameter below is optional (note the ?
), so it could possibly be undefined
. Therefore, it cannot be called directly:
function doSomething(callback?: () => void) {
// Error: Object is possibly 'undefined'.
callback();
}
Similar to checking objects before accessing a property, we need to check first whether the function has a non-null value:
function doSomething(callback?: () => void) {
if (callback) {
callback();
}
}
You can also check the value returned by the typeof
operator, if you prefer:
function doSomething(callback?: () => void) {
if (typeof callback === "function") {
callback();
}
}
#Summary
Non-nullable types are a fundamental and valuable addition to TypeScript's type system. They allow for precise modeling of which variables and properties are nullable. A property access or function call is only allowed after a type guard has determined it to be safe, thus preventing many nullability errors at compile-time.
This article and 44 others are part of the TypeScript Evolution series. Have a look!