Core types
Chapter 02 — Core types: the everyday vocabulary

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.
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: thepushcompiles. Tuple length is enforced on assignment, not onpush— decide whether that surprises you, then read each error code, not just each message.)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 runnpx tsc --noEmittwice: once with the kit’s"strict": true(refused: TS2454, used before being assigned) and once after flipping it tofalse(compiles clean, dies at runtime — video 084’s gotcha exactly; flip it back after). That pair is the whole argument for leavingstricton: the check you just watched fail open is the one Chapter 01 told you never to switch off.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 — theasline is you telling the compiler you know better. Name when that promise is earned and when it isanywith better manners.)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 assigningrto astringis a compile error but passingprintwhere a() => voidcallback 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:
- The Basics (inference, annotations)
- Everyday Types (primitives, arrays, tuples, enums, unions, literals)
- Narrowing (unions in,
unknownat the door) - More on Functions (
neverandvoidreturns) - Object Types (handoff to Chapter 03 — intersections live there)
Videos — the chapter’s shelf, section-mapped (quoted lines are the instructor’s own words as preserved in each companion entry):
- Collections: 083 — Objects and Arrays (“the types that we assign into that array cannot then be changed later on”) · 005 — Objects and Arrays · 032 — Datatypes in TypeScript
- Inference vs annotation: 082 — Type Basics (“it infers the type based on the value we assign it”) · 084 — Explicit Types (“like Luigi, it’s not gonna let me do this”; “cannot read property push of undefined”) · 004 — Type Basics · 006 — Explicit Types
- Unions, literals, aliases: 033 — Union Type · 034 — Literal Type · 035 — Understanding Type Alias
- The specials: 051 — Unknown Type · 052 — Never Type
- Full-hour companion: 079 — TypeScript for Beginners, Part B (type fundamentals end to end)
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.