Sebastian De Deyne
Designer & developer at Spatie

When to add types and when to infer in TypeScript

Type inference is the ability to derive types from other pieces of code. TypeScript’s type inference is very powerful, even a minimal amount of typing adds a lot of assertions.

Just because you don’t need to add types, doesn’t mean you shouldn’t. This is how I decide when or when not to explicitly add types in TypeScript.

In this first example, the least amount of types possible were added: the parameters.

function divide(a: number, b: number) {
    return a / b;
}

const result = divide(10, 2);

TypeScript infers that result is a number despite not adding a return type to divide, or a type declaration to the result variable.

TypeScript knows that a / b will return a number, so divide returns a number. TypeScript knows that result has the same type of whatever divide returns, which is a number.

A more explicit version would have a lot more number.

function divide(a: number, b: number): number {
    return a / b;
}

const result: number = divide(10, 2);

When do I add types, and when do I infer? I follow one simple rule: Add types to all function declarations.

My version of the snippet lands somewhere between the two.

function divide(a: number, b: number): number {
    return a / b;
}

const result = divide(10, 2);

I type function declarations for two reasons: readability and contract.

Readability: by typing the declaration, I know exactly what to expect of the function without looking at its body.

While we could assume this will return a number, we can’t be sure. And assumptions are exactly what we’re trying to avoid by using types.

function divide(a: number, b: number)

Contract: by typing the declaration, I’ll catch unexpected types in the function body sooner.

This would implicitly set the return type as void | number.

function divide(a: number, b: number) {
    if (b === 0) {
      return;
    }

    return a / b;
}

Instead, I want to ensure a number is returned.

function divide(a: number, b: number): number {
    if (b === 0) {
      throw new Error("Can't divide by 0");
    }

    return a / b;
}

While unexpected types like the above are easy to spot in small examples, they can be very subtle in large function bodies.

These rules might look arbitrary: why not add explicit types to variables?

In a way that’s true, but it’s important to maintain balance. We build software that changes over time. And the tighter you fasten the screws, the harder it is to loosen them.

If you enjoyed this post, you might be interested in my newsletter. I occasionally send a dispatch with personal stories, things I’ve been working on in the past month, and other interesting tidbits I come across online.