Chapter 02
Inference and assignability
How TypeScript works out types you did not write, and the rules that decide whether one type fits where another is expected.
Type inference
- What
- The compiler infers types from values:
const n = 3is3,let n = 3isnumber, and a function's return type comes from what it returns. - Why
- Annotate boundaries (parameters, exported functions, public APIs) and let inference handle locals. Less noise, and types stay in sync with the code.
const ids = [1, 2, 3];
let label = 'draft';
const byId = new Map<number, string>();
function double(n: number) {
return n * 2;
}Watch out
Leaving exported function return types to inference, so a small internal change silently alters a public API.
Structural typing
Also called duck typing
- What
- TypeScript compares types by shape, not by name. Any value with the required properties of the right types is assignable, whatever it was declared as.
- Why
- It matches how JavaScript is written: plain objects and classes interoperate freely, and you describe what a function needs rather than who made the value.
type Point = { x: number; y: number };
class Pixel { x = 3; y = 4; }
const p: Point = new Pixel();Watch out
Assuming two types with the same shape are kept apart; UserId and OrderId aliases of string are interchangeable.
Excess property checks
- What
- When you pass an object literal straight to a typed slot, extra properties the type does not declare are an error. The check does not apply to objects already stored in a variable.
- Why
- It catches misspelled option names such as
{ colour: 'red' }against{ color?: string }, where structural typing would otherwise accept it.
Watch out
Thinking this makes types exact: the same object passed through a variable is accepted with its extra properties.
Widening and as const
- What
- Mutable bindings widen literals:
let m = 'GET'isstring.as constkeeps the narrowest types and makes objects and arrays deeply readonly. - Why
- Use it for lookup tables and option lists so their values stay literal types you can derive unions from.
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number];The satisfies operator
- What
expr satisfies Tchecks that a value fitsTwithout changing the value's inferred type.- Why
- You get validation against a contract and keep the precise type, so a config object is checked for typos yet each key keeps its literal value.
type Theme = Record<string, `#${string}`>;
const theme = {
primary: '#4f46e5',
danger: '#dc2626',
} satisfies Theme;
theme.primary;Watch out
Using an annotation (const theme: Theme) where you wanted satisfies, and losing the known key names.
Type assertions and non-null !
Also called as, type cast
- What
value as Ttells the compiler to treat a value asT;value!says it is notnullorundefined. Neither does anything at runtime.- Why
- Occasionally you know more than the compiler, for example about a DOM element that is always present. Assertions are the escape hatch for those cases.
const input = document.querySelector('#email');
const email = (input as HTMLInputElement).value;Watch out
Asserting API responses (res.json() as User) and believing the data was checked; it was not.
Contextual typing
- What
- When a function or literal is written where a type is already expected, such as a callback argument, TypeScript infers its parameter types from that context.
- Why
- It is why
items.map(item => item.name)needs no annotation, and why moving a callback out into a variable can suddenly produce implicitanyerrors.
const users = [{ name: 'Ada' }, { name: 'Linus' }];
const names = users.map((u) => u.name);