Clean TypeScript code with type guards


What are type guards?

Type guards in TypeScript are special functions which are used to detect if any or unknown types are of a certain type.

How many times did you checked if a variable is a string or a number? Well, stop doing it by hand everytime, use type guards!

Being functions, those type checks are reusable and will prevent you from explicitely type cast anything in your code, leaving everything cleaner and easier to maintain and reuse.

How can they help you?

Well, TypeScript is a superset of JavaScript, isn't is? JavaScript, since is dinamically typed, could be quite dangerous if you do not type checking.

Type checking is one of the most boring and usually messier part of any codebase.

Type guards can help you by reducing your code base and reusing every type check you make.

Example using type guards

Instead of writing checks like this every time, which mess up your code

interface User {
    name: string;
    surname: string;
}

function fullName(arg: unknown): string {
    if (typeof arg === 'object' && arg !== null && 'name' in arg && 'surname' in arg) {
        const name = typeof (<User> arg).name === 'string' ? (<User> arg).name : '';
        const surname = typeof (<User> arg).surname === 'string' ? (<User> arg).name : '';
        
        return [name, surname].concat(' ');
    }

    return 'unknown';
}

use a type guard like this!

interface User {
    name: string;
    surname: string;
}

function isUser(arg: unknown): arg is User {
    return (
        typeof arg === 'object' &&
        arg !== null &&
        'name' in arg &&
        'surname' in arg
    );
}

function isString(arg: unknown): arg is string {
    return typeof arg === 'string';
}

function fullName(arg: unknown): string {
    if (isUser(arg)) {
        const name = isString(arg.name) ? arg.name : '';
        const surname = isString(arg.surname) ? arg.name : '';
        
        return [name, surname].concat(' ');
    }

    return 'unknown';
}

Great, cleaner and more reusable code!

Note that everytime you will have to do a check on a type implementing the User interface, you will have only to use the proper type guard function!

Advanced typeguards

You can of course write advanced typeguards for more structured data in order to grant if a variable is of a type or not.

For example you could check if a variable is a Person like this

// primitive-typeguards.ts
export function isNumber(arg: unknown): arg is number {
  return typeof arg === 'number';
}

export function isString(arg: unknown): arg is string {
  return typeof arg === 'string';
}
// Person.ts
import { isNumber, isString } from './primitive-typeguards';

export interface Person {
  age: number;
  name: string;
}

export function isPerson(arg: unknown): arg is Person {
  return (
    typeof arg === 'object' &&
    'age' in arg &&
    'name' in arg &&
    isNumber((arg as any).age) &&
    isString((arg as any).name)
  );
}
// app.ts
import * as person from './Person';

export default function getPerson(surname: string, name: string): Promise<person.Person | null> {
  const json = await (await fetch(`endpoint/${surname}/${name}`)).json();
  
  return person.isPerson(json) ? json : null;
}

Hint!

When you write your application using Domain Driven Development (aka DDD), it's a good practice to add, inside your entity's file, the proper type guards functions.

Another example for you

export interface User {
  name: string;
  surname: string;
}

export type t = User;

export const make = (partial: Partial<t> = {}): t => ({
  name: partial.name ?? '',
  surname: partial.surname ?? '',
})

export const guard = (arg: unknown): arg is t => (
  typeof arg === 'object' &&
  arg !== null &&
  'name' in arg &&
  'surname' in arg
)

Other blog entries