Advanced Static Types in TypeScript

This course explores the capabilities of TypeScript’s type system and shows how to use advanced static types in practice.

Watch the Course

TypeScript 2.0: Non-Nullable Types

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:

Domains of TypeScript's number type

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:

Domains of TypeScript's number, null, and undefined types

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.

Learn Node