TypeScript63 min total · 21 parts
TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime
Part 3 of 21 · ~2 min
Basic Types and Type Inference
Most of the time you don't write a type at all — you write a value, and TypeScript works out what it must be from the assignment itself:
let pendingCount = 0; // inferred: number
const queueName = "engineering"; // inferred: string
let queueOpen = true; // inferred: boolean
const stageIds = [101, 102, 103]; // inferred: number[]
const mixedBatch = [101, "ats-8841", true]; // inferred: (string | number | boolean)[]
That inference is almost always exactly right, which is why hand-annotating every local variable in a real TypeScript codebase is mostly wasted effort rather than a safety measure. The place inference genuinely runs out of information is a function's own parameters:
function daysInStage(startedAt) { // TypeScript has nothing to infer this from
return Math.floor((Date.now() - startedAt) / 86_400_000);
}
There's no assignment for TypeScript to read backward from here — a caller could hand daysInStage anything at all, and nothing in the body forces a particular type before you add one yourself. Left alone, the parameter becomes any (or an error, depending on a setting called noImplicitAny, covered later in the tsconfig.json chapter). startedAt should almost certainly be number if it's a timestamp — though Date is a reasonable alternative, and deciding between the two is precisely the kind of judgment call inference can't make for you, since both are plausible and only the rest of the codebase actually knows which one is expected.
// TypeScript can infer a parameter's type from context, even with no annotation —
// it just needs somewhere to read that context from
stageIds.map((id) => id * 2); // `id`: number, inferred from Array<number>.map's own signature
The one place this generosity becomes a real liability is a function's return type, specifically once other code starts depending on it:
export function priorityScore(daysWaiting: number) {
if (daysWaiting > 5) return "high";
return 0; // meant to return '"low"' here, typed a number by mistake instead
}
// TypeScript infers the return type as: string | number — technically accurate, and it hides the bug completely
Nothing above fails to compile. TypeScript infers exactly what the function does, typo included, rather than what it was supposed to do — the function isn't wrong about its own contract, the contract is just wrong. Every caller now inherits a string | number they weren't expecting, with no error anywhere pointing at the actual mistake. An explicit : string on the signature turns that same typo into an error right where it happened, instead of a surprise three call sites away.