Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Defensive Programming

Do not fall back to default

Whenever possible, cover all cases. If a new case is added in the future, TypeScript should force the developer to set behavior for it.

Force explicit return types

Makes sure all cases are covered in a function.

// TS Error: Function lacks ending return statement and return type does not include 'undefined'
export const isEnabled = (status: 'a' | 'b' | 'c'): boolean => {
    if (status === 'a') {
        return true;
    }

    if (status === 'b') {
        return false;
    }
};

Use exhaustive switch

Makes sure all cases are covered in a switch statement.

// TS Error: Argument of type '"c"' is not assignable to parameter of type 'never'
export const isEnabled = (status: 'a' | 'b' | 'c') => {
    switch (status) {
        case 'a':
            return true;
        case 'b':
            return false;
        default:
            return exhaustive(status);
    }
};

Use type-mapping technique

Alternative to an exhaustive switch statement.

type Schema = {
    a: number;
    b: number;
};

// TS Error: Property 'b' is missing in type '{ a: () => string; }' but required in type '{ a: () => void; b: () => void; }'.
const result: { [K in keyof Schema]: () => void } = {
    a: () => console.log('This is A'),
};

Do not use exceptions

Unless failures are unpredictable, pass errors via return and do not throw. Throwing exceptions is not type-safe. There is a Result type that shall be used.

Bad:

try {
    const result = await action();
} catch (error) {
    // Possible errors cannot be typed
    // ...
}

Good:

const result = await action();

if (result.error) {
    const { type } = result.error;

    switch (type) {
        case 'ErrorA':
        // ... do stuff
        case 'ErrorB':
        // ... do different stuff

        default:
            return exhaustive(type);
    }
}