In any Typescript project you will inevitably end up working with an unknown data type that you need to know its correct type to proceed with confidence. One strategy for ensuring type correctness is with type guards.

What are type guards?

According to the Typescript docs:

A type guard is some expression that performs a runtime check that guarantees the type in some scope.

In practice, a type guard is simply a function that accepts some value as an argument and returns a boolean. The function also has a special return type in this format: value is SomeType. If the function returns true, then the value passed in is the type SomeType. If the function returns false, then it’s not. This is especially useful for narrowing a union type or validating data returned from an API.

Basic building blocks

Some basic type guards can make any project more readable and reduce duplicated code. Starting with a few native JavaScript data types we can have the building block type guards for more complex types.

The following are type guard examples for the most basic and common data types: array, boolean, null, number, object, string, undefined.

function isArray(value: unknown): value is Array<unknown> {
return Array.isArray(value)
}
function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean'
}
function isNull(value: unknown): value is null {
return value === null
}
function isNumber(value: unknown): value is number {
return typeof value === 'number'
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && !Array.isArray(value) && value !== null
}
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function isUndefined(value: unknown): value is undefined {
return typeof value === 'undefined'
}

These basic type guards are super helpful in variety of use cases — notably when discerning an optional type while also improving readability. The first statement uses a reusable type guard and is easier to read.

if (isUndefined(foo)) {
// Do something
} else {
// Do something else
}
// versus
if (typeof foo === 'undefined') {
// Do something
} else {
// Do something else
}

Layering complexity

Consider the following nested type for a Song:

type Song = {
title: string
album: Album
}
type Album = {
name: string
releaseDate: string
band: Band
}
type Band = {
name: string
members: string[]
startYear: number
endYear: number | null
}

For example:

const song: Song = {
title: 'Tomorrow Never Knows',
album: {
name: 'Revolver',
releaseDate: '5 August 1966',
band: {
name: 'The Beatles',
members: [
'John Lennon',
'Paul McCartney',
'George Harrison',
'Ringo Starr',
],
startYear: 1960,
endYear: 1970
}
}
}

While we could write a single type guard to check this song object, it’s much easier to break up the object into its primary parts starting with the most deeply nested object. In this case, that’s the members array.

function isMembers(value: unknown): value is string[] {
if (!isArray(value)) {
return false
}
return value.every((item) => isString(item))
}

Now we have all the pieces to move up to the Band object by using our basic type guards along with our isMembers type guard.

function isBand(value: unknown): value is Band {
if (!isObject(value)) {
return false
}
return (
isString(value.name) &&
isMembers(value.members) &&
isNumber(value.startYear) &&
(isNumber(value.endYear) || isNull(value.endYear))
)
}

Next, let’s move up to the Album object.

function isAlbum(value: unknown): value is Album {
if (!isObject(value)) {
return false
}
return (
isString(value.name) && isString(value.releaseDate) && isBand(value.band)
)
}

Finally, we can write our Song type guard. And it’s trivial to check for list of songs as well.

// Song type guard
function isSong(value: unknown): value is Song {
if (!isObject(value)) {
return false
}
return isString(value.title) && isAlbum(value.album)
}
// Song list type guard
function isSongList(value: unknown): value is Song[] {
if (!isArray(value)) {
return false
}
return value.every((item) => isSong(item))
}

What's great about this technique is that it is easy to reason about. They're also easy to write tests for. Also, you never have to worry about the argument type since we just label that as unknown and you never have to cast an object in order to read a property off of it.

A note on very large objects

This layering approach works well when objects aren't too large. For larger and more complicated types, it might be a good idea to create a JSON Schema file and validate it in your type guard with a library like Ajv.