3.2 Type Inference: When You Can Omit the Annotation
Look, I get it. You’re busy. Writing : number after every variable feels like filling out tax forms in triplicate. The good news is, TypeScript’s type inference is shockingly good. It’s the language’s way of saying, “I see what you’re doing, I got this.” You can often just shut up and let it do its job.
The Beautiful Simplicity of Initialization
This is the most common and most reliable scenario. When you declare a variable and immediately assign a value to it, TypeScript locks in the type of that value as the variable’s type. It’s a one-and-done deal.
let myAge = 35; // TypeScript infers `number`
const myName = "Ada"; // Infers the literal type "Ada" (more on this in a sec)
let isProgramming = true; // Infers `boolean`
// Try to assign something else later? Nope.
myAge = "thirty-five"; // Error: Type 'string' is not assignable to type 'number'.
Why this works so well is that the initial assignment provides a perfect, concrete example of what you intend the variable to be. The type isn’t a vague hope; it’s a fact established at birth.
const vs. let: A Subtle But Important Difference
Pay close attention here, because this is where people get tripped up. Notice I used const for myName and let for myAge. This matters.
With let, TypeScript infers the general primitive type, like string or number. It assumes the value might change.
With const, the value can’t be reassigned. Because of this, TypeScript can infer a more specific type: the literal value itself. It’s not just a string; it’s the string "Ada".
// Using `let` (value can change)
let framework = "React"; // Type: string
framework = "Svelte"; // This is fine, it's still a string.
// Using `const` (value cannot change)
const language = "TypeScript"; // Type: "TypeScript" (the literal type)
language = "JavaScript"; // Error: Cannot assign to 'language' because it is a constant.
This literal type inference is incredibly powerful and is the foundation for how TypeScript’s entire type system works, especially with unions and discriminated unions. You get more precise types for free, just by using const.
When Inference Needs a Nudge: The Any Escapes
TypeScript isn’t psychic. It can only infer types from the information you give it. If you don’t provide any clues at birth, it has to give up and give you any—the type system’s “off” switch. This is the most common pitfall.
The biggest culprit? Empty arrays and uninitialized variables.
// Pitfall 1: Empty arrays
let items = []; // Type: any[]
items.push(1); // [1]
items.push("shoes"); // [1, "shoes"] -- this is now a mess.
// Pitfall 2: No immediate initialization
let guessMyType; // Type: any
guessMyType = 10; // fine
guessMyType = "hello"; // also fine, because it's still `any`
This is where you must use an explicit type annotation. It’s not optional; it’s critical for maintaining type safety.
// The Fix: Annotate to avoid `any`
let items: number[] = []; // Now it's a number[]
items.push(1); // Good.
items.push("shoes"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
let guessMyType: number; // Now it's a number
guessMyType = 10; // Good.
guessMyType = "hello"; // Error.
Function Return Types: To Annotate or Not to Annotate?
This is a religious debate, and I’m here to preach. You should almost always explicitly annotate your function return types.
Why? Two reasons:
- It’s a contract. It ensures you don’t accidentally change the return type of your function while refactoring its body. The function implementation must satisfy the declared return type, which acts as a fantastic safety net.
- It’s documentation. It tells anyone reading your code (including future you) exactly what to expect without them having to mentally execute the function’s logic.
// Without annotation (TS infers `number`)
function add(a: number, b: number) {
return a + b;
}
// With annotation (We declare it returns `number`)
function add(a: number, b: number): number {
return a + b;
}
// The benefit becomes obvious with more complex functions:
function getProgrammerName(): string {
// ... complex logic ...
// If I accidentally return a number here, TS will catch it because of the annotation.
return "Ada Lovelace";
}
Inference is fine for tiny, obvious functions, but for anything non-trivial, write the return type. Your colleagues will thank you. You will thank you.
The Golden Rule
Trust inference when the value is provided upfront. It’s clean and correct. Use explicit annotations when the value comes later (uninitialized variables, empty arrays) or when you need to declare a contract (function returns). This isn’t about being lazy; it’s about being smart and letting the tool work for you.