Returning to TypeScript: the language
A practical refresher. Read the contract, trace the examples, and attempt the exercise before opening its answer. Code blocks may share context; only ts check blocks are standalone checks.
Types and values
A value exists at runtime; a type describes permitted values to the checker. Ordinary annotations, type aliases and interfaces are erased, so they do not validate JSON. Some TS constructs such as a normal enum generate runtime code. Use lowercase string, number and boolean rather than wrapper types. number includes integers and fractions; bigint is separate and cannot be mixed directly with number arithmetic.
const limit: number = 4;
type Options = { limit: number };
Inference and widening
let can be reassigned, so a string literal typically widens to string. const keeps a literal for a primitive binding, but an object property remains mutable and often widens. Add an explicit contract when building a state object.
as const narrows literals and produces readonly views; satisfies checks the shape but may retain a narrow literal instead of widening it.
Give empty collections context when incrementally filling them; do not memorize a universal rule that every empty array is never[].
let status = "queued";
const mode = "queued";
const job = { status: "queued" };
type Job = { status: "queued" | "done" };
const job: Job = { status: "queued" };
job.status = "done";
const state: { tags: string[] } = { tags: [] };
state.tags.push("new");
Structural typing
Compatibility normally depends on members rather than the declared name. An object with required members and additional fields may be assignable through a variable. A fresh object literal also receives excess-property checks to catch likely mistakes. Those checks do not make an exact runtime schema or remove extra fields. Two aliases of string remain compatible; brands can distinguish IDs when this matters.
type Identified = { id: string };
function show(value: Identified): string {
return value.id;
}
const product = { id: "A", price: 500 };
show(product);
type Identified = { id: string };
function show(value: Identified): string { return value.id; }
// @ts-expect-error: intentional type error
show({ id: "A", price: 500 });
Unions and intersections
A | B allows one of the alternatives; only operations safe for all members are available until narrowing. A & B must satisfy both contracts simultaneously, so conflicting property types can produce never.
An intersection does not overwrite a property with the right side: omit the original key first when replacing its type. Model mutually exclusive states as a union.
function format(value: string | number): string {
return typeof value === "number" ? value.toFixed(2) : value.trim();
}
type Impossible = { id: string } & { id: number };
Optional versus undefined
With exactOptionalPropertyTypes, field?: number permits absence but not an explicit undefined unless included in the value union. Reading the field can still produce undefined. field: number | undefined requires the property itself. PATCH often assigns different meanings to absence and null. in includes inherited properties; Object.hasOwn checks ownership but is not always enough to narrow the value type.
type Options = { retries?: number };
const omitted: Options = {};
Arrays, tuples and readonly
An ordinary array has no minimum length. Under noUncheckedIndexedAccess, an indexed read can be undefined. A tuple describes fixed positions, and labels are editor information rather than runtime named fields. Prefer readonly inputs for functions that only read: both mutable and readonly callers can use them. Readonly is shallow and another alias can still mutate the underlying object.
const prices: number[] = [100, 200];
const point: readonly [x: number, y: number] = [10, 20];
Function signatures
Context provides callback parameter types. An optional parameter means callers may omit it; do not mark index optional when the API always passes it simply because some callbacks ignore it. Rest parameters collect arguments. Async functions return Promise<T>. A callback typed () => void may return a value because the caller promises not to use it; void is not a runtime proof that nothing is returned.
type Formatter = (price: number, currency?: string) => string;
const format: Formatter = (price, currency = "USD") => `${currency} ${price}`;
This and closures
A normal method gets this from its call site and can lose its receiver when passed as a callback. bind, a wrapper or an arrow field can preserve the intended receiver. A closure retains access to a variable rather than an automatic snapshot. let provides an iteration-local binding; var can make callbacks share one changing binding.
class Counter {
value = 0;
increment() { this.value++; }
}
const counter = new Counter();
const safe = () => counter.increment();
safe();
Passing counter.increment to a scheduler detaches the method from its object. The receiver is determined by the later call, not by where the method was obtained.
bind creates a function with the receiver attached; a wrapper calls the method on the object when needed.
A closure reads the binding when invoked, so changing count before the call changes the result.
Check both a direct call and a detached callback: testing only counter.increment() cannot expose the bug.
class Counter {
value = 0;
increment() { this.value++; }
}
const counter = new Counter();
const detached = counter.increment;
// detached() would throw in this strict module: this is undefined.
const bound = counter.increment.bind(counter);
bound(); // value is 1.
let count = 0;
const readCount = () => count;
count = 2;
readCount(); // 2: the closure reads the current variable, not its old value.
Practice
Why can an inferred job status fail a literal-union contract? Why can Readonly<{ tags: string[] }> still permit push? How does an optional field differ from a required field containing undefined?
Spoiler
The inferred property can be string; annotate the intended state contract. Readonly prevents replacing tags but its array needs readonly string[] to prevent mutation through that view. An optional property may be absent; the required union property must exist. exactOptionalPropertyTypes additionally controls explicitly writing undefined.
Move on when you can explain
Explain type versus value, write a callback signature, distinguish missing from null, and narrow unknown data without assertions. Then use the runtime chapter and practical TypeScript chapter to put these rules into working code.