TypeScript course menu
TypeScript Type Inference: When to Annotate and When to Let TypeScript Decide
Learn how TypeScript infers types from values, return statements and context: let vs const literal inference, inferred return types, contextual typing of callbacks, best common type for arrays, and guidelines on when explicit annotations help.
On this page
TypeScript does not need an annotation on every line. It infers types from the code: if you write let count = 0, it knows count is a number. Good TypeScript code relies on inference most of the time and adds annotations where they add value.
Inferring from values#
let city = "Pune"; // string
let rating = 4.5; // number
let isOpen = true; // boolean
city = "Delhi"; // fine: still a string
console.log(city, rating, isOpen);Delhi 4.5 trueThe inferred type is just as strict as a written one:
let rating = 4.5;
rating = "excellent";error TS2322: Type 'string' is not assignable to type 'number'.Hover over a variable in VS Code to see what type TypeScript inferred.
let versus const#
A const can never change, so TypeScript infers the exact value as its type (a literal type). A let can be reassigned, so it gets the general type:
let size = "medium"; // type: string
const level = "beginner"; // type: "beginner"
size = "large";
console.log(size, level);large beginnerLiteral types become important for unions such as "small" | "medium" | "large"; see Literal Types.
Inferring return types#
TypeScript infers what a function returns:
function average(values: number[]) { // return type inferred as number
return values.reduce((a, b) => a + b, 0) / values.length;
}
const result = average([80, 90, 70]); // result: number
console.log(result.toFixed(1));80.0Parameters, however, cannot be inferred from how the function is used. Without an annotation, a parameter is an error in strict mode:
function double(n) {
return n * 2;
}error TS7006: Parameter 'n' implicitly has an 'any' type.Contextual typing#
When a function is passed somewhere that already expects a certain function type, TypeScript infers its parameters from that context:
const names = ["asha", "ravi", "meera"]; // string[]
const capitalised = names.map((name) => name[0].toUpperCase() + name.slice(1));
// ^ name is inferred as string
console.log(capitalised);[ 'Asha', 'Ravi', 'Meera' ]This is why callbacks to map, filter, addEventListener and so on rarely need annotations.
Arrays and objects#
const scores = [72, 95, 58]; // number[]
const mixed = [1, "two", 3]; // (string | number)[]
const user = { name: "Asha", age: 21 }; // { name: string; age: number }
user.age = 22;
console.log(scores.length, mixed.length, user);3 3 { name: 'Asha', age: 22 }For an empty array, TypeScript cannot know what will go into it, so annotate it: const items: string[] = [];.
When to annotate#
A useful habit: write the types you mean at boundaries (parameters, return values, data coming in), and let inference handle the rest.
Practice#
Practice exercise
For each variable, say what type TypeScript infers, then check with the compiler by assigning a wrong value to one of them:
let title = "TypeScript";
const version = 5;
const tags = ["types", "compiler"];
const course = { name: "TS", lessons: 39 };Show solution
title:string(alet, so the general type).version:5(aconst, so the literal type).tags:string[].course:{ name: string; lessons: number }.
const tags = ["types", "compiler"];
tags.push(42);error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.Key takeaway#
Finished this lesson?