TypeScript

Sefism.

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 = 3 is 3, let n = 3 is number, 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.
TypeScript
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.
TypeScript
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' is string. as const keeps 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.
TypeScript
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number];

The satisfies operator

What
expr satisfies T checks that a value fits T without 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.
TypeScript
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 T tells the compiler to treat a value as T; value! says it is not null or undefined. 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.
TypeScript
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 implicit any errors.
TypeScript
const users = [{ name: 'Ada' }, { name: 'Linus' }];
const names = users.map((u) => u.name);
02 · Inference and assignability2 / 10