Chapter 29: Type Inference
Core Idea
Beyond simple "infer from the initializer" cases, TypeScript infers types in two more subtle ways worth understanding explicitly: picking a best common type across multiple candidate expressions (e.g. array elements), and contextual typing, where the expected type flows backward from where an expression is used into the expression itself.
Key Concepts
- Best common type: when inferring from multiple expressions (classically, array literal elements), TypeScript looks for a candidate type compatible with every element.
[0, 1, null] infers as (number | null)[] because number is compatible with both number and null values structurally in that set.
- No single best common type → falls back to a union, or to no inference at all: if array elements are instances of unrelated-but-similarly-shaped classes with no single common supertype among the candidates themselves (even if they share an ancestor), TypeScript won't automatically reach for that ancestor — it either infers a union of the actual element types, or (in some cases) makes no useful inference, requiring an explicit annotation (e.g.
let zoo: Animal[] = [...]) to get the intended common-supertype element type instead of an auto-generated union.
- Contextual typing: type inference running "backward" — the expected type at an expression's location (assignment target, call argument, return statement, array/object literal member) flows into the expression itself. Assigning a plain function expression to
window.onmousedown lets TypeScript infer that function's parameter as a MouseEvent (with .button but no .kangaroo) purely from the declared type of onmousedown, with zero annotations written on the function itself.
- Contextual typing is location-sensitive: the same function expression shape assigned to a different contextually-typed slot (e.g.
window.onscroll instead of onmousedown) infers a different parameter type (UIEvent instead of MouseEvent) — the inference comes entirely from where the expression sits, not from anything intrinsic to the expression.
- Losing contextual typing loses safety: the same function assigned somewhere with no contextual type (e.g. a bare
const handler = function(uiEvent) {...}) gets an implicit any parameter instead — accessing a nonexistent property then silently compiles (unless noImplicitAny is on). An explicit any annotation on the parameter has the same suppressing effect, deliberately overriding the inferred contextual type.
- The contextual type also feeds into best-common-type inference: a function declared to return
Animal[] and returning an array literal of subclass instances uses Animal as an additional best-common-type candidate (from the return-type context), which is exactly what lets the array literal infer as Animal[] even though none of its own individual elements are literally Animal instances.
Code Examples
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.button); // OK — inferred as MouseEvent from context
};
window.onscroll = function (uiEvent) {
console.log(uiEvent.button); // Error — inferred as UIEvent, no `.button` property
};
- What it demonstrates: the same-shaped function expression getting two different contextually-inferred parameter types purely based on which handler property it's assigned to.
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()]; // element type: Animal, via return-type context
}
- What it demonstrates: a declared return type supplying an extra best-common-type candidate, letting a mixed-subclass array literal infer as the shared base type instead of a union.
Key Takeaways
- When an array/object literal of mixed related-class instances doesn't infer the supertype you expect, add an explicit annotation (variable type, or a function's return type) — that annotation becomes a best-common-type candidate the inference algorithm can actually pick.
- Contextual typing is what makes untyped callback parameters (event handlers, array-method callbacks) safe without manual annotations — losing that context (assigning to a plain
const first, or explicitly typing a parameter as any) silently reverts to any and drops all checking on that parameter.
- Type inference in TypeScript isn't purely "look leftward at the initializer" — it also looks at surrounding context (assignment target, expected return type, expected argument type) to fill in what an expression's type should be.
Connects To
- Everyday Types: the basic "infer from initializer" case this chapter builds on.
- More on Functions: contextual typing is exactly why anonymous callback parameters usually don't need explicit annotations.
- Type Compatibility: the compatibility rules the best-common-type algorithm relies on to pick a candidate.