TypeScript Survival Guide

Course manual

Core types

Chapter 02 — Core types: the everyday vocabulary

Diverse teenage scouts practicing courteous introductions in a sunny meadow campsite

A small working set carries the entire course: primitives, arrays, tuples, enums, inference, unions, literals, and the four special types (any, unknown, never, void). This chapter defines each on first use, shows when to annotate and when to let the compiler infer, and gives exact when-to-use guidance for the four specials. The lab has you type functions and data.

Examples lead with the Troop 413 gear list, then show the same shapes cut from production: the Sumerian Squares stage types, where host identities, states, and tiers are modeled with exactly the vocabulary below.

Primitives, arrays, tuples, enums

Three primitives cover most values: string (text), number (all numbers — there is no separate integer type), and boolean (yes-or-no). Two more appear constantly: null (deliberately empty) and undefined (not yet assigned). Annotate state that starts empty with both the type and the emptiness: let cook: string | undefined.

Arrays hold lists of one type: string[] is a list of strings, and patrol.map((s) => s.name) returns string[] because the compiler tracks the element type through the call. A tuple fixes the length and each position’s type: [string, number] might pair a name with a bunk, and assigning a third element or swapping the order is a compile error. Reach for tuples for small fixed-shape pairs — coordinates, name-and-number — and arrays for everything of open length.

Enums name a closed set of choices. Production code models the stage lifecycle exactly this way:

// cut from assets/sumerian-squares/src/types.ts
export enum AppState {
	Poster = "poster",
	Loading = "loading",
	Idle = "idle",
	Speaking = "speaking",
}

String enums like AppState are preferred over numeric ones: they print readably in logs and survive reordering. A literal union ("poster" | "loading" | "idle" | "speaking") is the lighter alternative — Chapter 03 and the objects chapter revisit the tradeoff — but when the set drives behavior across files, an enum gives the set one name and one home.

New here: primitives, arrays, tuples, enums. Docs: Everyday Types. Watch: 083 — Objects and Arrays (“When we declare a new array, the types that we assign into that array cannot then be changed later on” — push, index write, even reassignment away from array type, all fail), then 032 — Datatypes in TypeScript for the full primitive survey, and 005 — Objects and Arrays for the same ground in Net Ninja form.

Inference and annotations

TypeScript infers the type of most bindings from their initializer: const bunk = 7 is a number without any annotation, and widening rules keep let bindings flexible (let count = 0 stays number, not the literal 0). Inference is the default — write the value, let the compiler label it.

Annotations are for the places inference cannot see: function boundaries (parameters and return types), empty-start state, and any value crossing into your code from elsewhere. The rule for this course: annotate every function parameter and return type, annotate [] and null starts, and let inference handle the rest. An annotated signature is a contract the compiler checks at every call site:

export function hikers(names: string[]): number {
	return names.length;
}

const camp: string[] = [];
camp.push("Aiko"); // checked against the annotation

Over-annotating has a cost: every redundant : string is a label you must maintain when the code changes. Under-annotating has a worse one: an untyped boundary is where any creeps in (see below). Annotate the doors, infer the rooms.

New here: inference, widening, annotation placement. Docs: The Basics. Watch: 082 — Type Basics (“TypeScript uses what’s known as inference, or it infers the type based on the value we assign it” — no annotation written in the whole lesson; hovering proves each label), then 084 — Explicit Types for the other side: nothing to infer from yet, so “if I try to give it a value of a string, like Luigi, it’s not gonna let me do this, and we get an error” — plus the gotcha that earns its own drill below (let ninjas: string[] compiles, the first push throws: “cannot read property push of undefined”).

Unions, intersections, literals

A union says a value may be this OR that: string | number accepts either. Unions model field data — a bunk field that is a number once assigned but null before camp (number | null), a lookup that may miss (Scout | undefined). Narrow a union with a check (typeof, ===, or a truthiness test) and the compiler tracks which arm you proved.

An intersection says a value must be this AND that: Hitch & Anchor has every field of both. Intersections compose capabilities — a hitch that is also an anchor — and appear most when combining object types, the subject of Chapter 03.

A literal type allows exact values only. "circle" as a type means the string "circle" and nothing else — not "square", not string. Unions of literals are the workhorse of honest modeling, in the patrol register and in production:

// cut from assets/sumerian-squares/src/types.ts
export type HostType = "adult_female" | "adult_male" | "alien";

export interface DegradationTier {
	readonly name: "full" | "viewport" | "hero" | "poster";
	readonly maxHosts: number;
	readonly enableMorphs: boolean;
}

HostType lists the only three host kinds the stage knows; a fourth string fails compilation instead of rendering a broken scene. DegradationTier pairs a literal-union name with its numeric and boolean settings — the kind of small honest object you will write constantly after Chapter 03.

New here: unions, intersections, literals, narrowing. Docs: Narrowing. Watch: 033 — Union Type then 034 — Literal Type (the OR before the exact), plus 035 — Understanding Type Alias for giving sets like HostType one name. Shaun’s union seed from video 083 closes the loop: declare the variety up front and TypeScript “will look at this and say, OK, well this is an array now, which can contain strings or numbers”.

any, unknown, never, void: when to use each

any means “skip checking this value.” It is contagious — any operation on any is any — so one any silences the checker for everything it touches. When to use it: almost never. The only honest uses are deleting a legacy barrier one file at a time (annotate, then shrink it) and describing a value whose shape genuinely cannot be known (rare). If you reach for any, write a comment saying what the type is and when you will replace it. The course typecheck treats unexplained any as a failure.

unknown means “anything could arrive, so check before use.” It is the honest front door: JSON responses, form inputs, and parsed files arrive as unknown, and the compiler forces a narrowing check before any use. When to use it: every boundary where outside data enters. unknown plus a guard is the thesis of the whole course.

never means “impossible.” It is the return type of a function that never returns (it always throws) and the type of a value that cannot exist — the empty arm after you have handled every union member. When to use it: throw helpers and exhaustiveness checks, where the compiler proves you covered every case because anything left over is never.

void means “returns nothing useful.” It is the return type of callbacks run for their effect (patrol.forEach((s) => console.log(s.name))) and of functions whose result no caller should use. When to use it: effect-only functions. Never confuse it with undefined as a value — void is a statement about the function, not the data.

export function parseBunk(raw: unknown): number {
	if (typeof raw === "number" && Number.isFinite(raw)) return raw;
	if (typeof raw === "string" && raw.trim() !== "") {
		const parsed = Number(raw);
		if (Number.isFinite(parsed)) return parsed;
	}
	throw new Error("bunk must be a finite number");
}

export function printPatrol(names: string[]): void {
	names.forEach((n) => console.log(n));
}

parseBunk takes unknown at the door, narrows with checks, and throws (which returns never) on refusal. printPatrol returns void because its job is the printing, not a value.

New here: any, unknown, never, void. Docs: Narrowing (unknown plus a guard) and More on Functions (never and void return types). Watch: 051 — Unknown Type then 052 — Never Type (the honest door before the impossible), and 006 — Explicit Types for annotate-now-assign-later boundaries in Net Ninja form.

DIY simulator drills

You are the simulator. Cover the answers, predict, then verify. Log each miss in one sentence.

  1. Be the compiler. Under strict: true, which lines error?

    let bunk: [string, number] = ["Aiko", 3];
    bunk = [3, "Aiko"];
    bunk.push("Zola");
    type Host = "a" | "b";
    const h: Host = "c";
    

    Verify with npx tsc --noEmit. (Three complaints on two lines — the swap fails per position, the fourth string fails the union. Plus one honest surprise: the push compiles. Tuple length is enforced on assignment, not on push — decide whether that surprises you, then read each error code, not just each message.)

  2. Be the runtime. What prints, and what throws?

    let ninjas;
    ninjas.push("Luigi");
    

    Verify with node -e (TypeError: cannot read property push of undefined). Then add the annotation — let ninjas: string[] — and run npx tsc --noEmit twice: once with the kit’s "strict": true (refused: TS2454, used before being assigned) and once after flipping it to false (compiles clean, dies at runtime — video 084’s gotcha exactly; flip it back after). That pair is the whole argument for leaving strict on: the check you just watched fail open is the one Chapter 01 told you never to switch off.

  3. Be the narrower. Which lines compile when the door is unknown?

    declare const raw: unknown;
    const a = raw + 1;
    if (typeof raw === "number") {
    	const b = raw + 1;
    }
    const c = raw as number;
    

    Verify with npx tsc --noEmit. (One refuses, one compiles because you proved it, one compiles because you promised it — the as line is you telling the compiler you know better. Name when that promise is earned and when it is any with better manners.)

  4. Be the void. What is r — type and value?

    function print(names: string[]): void {
    	names.forEach((n) => console.log(n));
    }
    const r = print(["Aiko"]);
    

    Verify: hover r (type), run it (value). Then explain why assigning r to a string is a compile error but passing print where a () => void callback is expected is fine.

Lab: type functions and data

The typing-set kit gives you untyped patrol functions — a bunk label, a hiker count, a parser for raw input — with a red suite describing the contracts. Add annotations at every boundary, replace the any placeholders with unknown plus narrowing, and model the fixed choices (position, status) as literal unions. Green means the tests pass and tsc --noEmit is silent.

Resources

Docs, in the order the chapter introduces them:

Videos — the chapter’s shelf, section-mapped (quoted lines are the instructor’s own words as preserved in each companion entry):

Toolchain and runtime videos live on Chapter 01’s shelf.

Hands-on: drill set above (predict first, tools second), then the typing-set kit — annotate every boundary, unknown plus narrowing at each door, literal unions for the fixed choices.