Functions and generics
Use inferred and explicit return types
Let simple implementation details infer their result and annotate public boundaries when the promised return type matters.
TypeScript infers a return type by looking at every return statement in the function. You rarely have to write one. For small local functions, inference is all you need.
Here the result is inferred as number:
function total(values: number[]) {
return values.reduce((sum, value) => sum + value, 0)
}
Hover over total in your editor and you see (values: number[]) => number. The compiler worked that out from the reduce() call and its 0 seed. Nobody wrote number anywhere near the return.
When to annotate
An explicit return type is useful when the function is a public contract, something other files or other people depend on:
function total(values: number[]): number {
return values.reduce((sum, value) => sum + value, 0)
}
The difference shows up when the implementation changes. Suppose someone edits the function to return 'none' for an empty array:
function total(values: number[]): number {
const sum = values.reduce((sum, value) => sum + value, 0)
return values.length ? sum : 'none'
}
Without the annotation, the inferred return type quietly becomes string | number. Nothing fails here. The errors appear in every caller that does arithmetic with the result, in files the person editing never opened.
With : number, the error appears inside the function, at the bad return:
error TS2322: Type 'string' is not assignable to type 'number'.
The person who made the change sees the error, in the file they were editing, on the line they just wrote. That locality is the main argument for annotating exported functions.
Annotations also help in two other cases. A recursive function sometimes cannot infer its own result, because the return type depends on itself. And a function with several branches makes it easy to miss a path that returns undefined. Writing the return type forces you to decide whether every path returns a value.
When not to annotate
Do not annotate every small local function. Inference keeps implementation code readable, and it often preserves a more precise type. A function that returns 'asc' infers the literal type 'asc'. A hasty : string annotation throws that precision away.
My rule: annotate the exported surface of a module, let the internals infer. Public functions get a return type, because callers depend on it. Helpers inside the file do not, because I can see all their callers.
Try this on your own: add an empty-array branch returning 'none' to the unannotated total() and hover over it to see the widened type. Then add : number back and read the error.
Lesson completed