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.