Objects
Chapter 03 — Objects: modeling the domain

Most values worth checking are objects: knots with names and loop counts, hosts with accents and shapes, tiers with settings. This chapter covers how TypeScript describes object shapes — object types, interfaces, and aliases — how to mark fields optional, read-only, or open-ended, how to extend and compose shapes, and the structural rule that makes it all work. The lab has you model the knot rack as a domain.
Examples lead with trail knot shapes, then show the production cuts they mirror: host configs and the host pool from the Sumerian Squares stage, where the same four techniques describe a shipping 3D cast.
Object types, interfaces, aliases
An object type lists each field and its type:
export type KnotEntry = {
name: string;
use: string;
material: string;
loops: number;
};
An interface declares the same shape with a name the compiler can extend and error messages can quote:
export interface Knot {
name: string;
use: string;
material: string;
loops: number;
}
A type alias names any type — object, union, literal, or combination — so
it can be reused and discussed: export type Loops = number | null;.
Course convention: interfaces for object shapes that represent domain
things (knots, troop records, configs), aliases for unions, literals, and
compositions. Either can describe an object; the difference that matters is
that interfaces extend (see below) while aliases compose with &. Pick
by that need, not by habit.
New here: object types, interfaces, aliases. Docs: Object Types and Interfaces. Watch: 015 — Interfaces (a contract is not a class: no constructor, never generates objects — it only declares what future variables, objects, and function parameters must satisfy), then 093 — Interfaces in Practice (“say I have a constant called me and I’m gonna set that type to be IsPerson” — the contract applied to a value, then to a function parameter), and 104 — Type vs Interface for the course’s own convention under fire — Kyle: the handbook is “completely backwards because interfaces are more complicated than types, they have less features than types.” Know his argument; the course still picks interfaces for domain things, and now you know exactly what that choice costs.
Production code follows the same split — interfaces for the domain things, aliases for the closed vocabularies:
// cut verbatim from the Sumerian Squares stage sources (sister repo)
export type HostId =
| "cristine"
| "fiona"
| "grace"
| "maya"
| "alien"
| "luke"
| "jay"
| "preston"
| "wes";
export interface HostConfig {
readonly id: HostId;
readonly position: number; // 1-9 grid position
readonly archetype: string;
readonly gltfUrl: string;
readonly hostType: HostType;
}
HostId is an alias over a nine-member literal union — the closed set of
performers. HostConfig is an interface using it: each host carries its
grid position, archetype name, model URL, and kind. Your knot model will
look like this by the end of the lab.
Optional, readonly, index signatures
Three modifiers adjust fields. Optional (loops?: number) marks a field
the value may omit — a coil before its loops are counted. Reading it gives
number | undefined, so the compiler forces the check. Keep optionals
honest: only fields genuinely absent some of the time earn the ?.
readonly marks a field assignable once, at creation, and never again.
Knot entries, host configs, and tier settings are all read-only after
build — mutation is where shared references rot, so the course marks domain
data readonly by default and drops the modifier only where mutation is
the point.
Index signatures describe open-ended dictionaries: objects whose keys are not known up front but whose values share a type:
export interface CampGrades {
readonly [name: string]: number;
}
const grades: CampGrades = { Bowline: 95, "Clove hitch": 92 };
Use index signatures for lookup tables keyed by runtime data (names,
host ids). Prefer explicit fields when the keys are known — a typo in a
known key should be a compile error, not a silent new entry. A
Record<HostId, PooledHost> would demand all nine hosts up front with
a missing host failing the build; production code reaches for the
runtime cousin instead — const pool = new Map<HostId, PooledHost>()
in host-pool.ts — because the pool evicts and refills while the
stage runs. Same closed vocabulary as the key (HostId), different
container for a different lifetime: Record for fixed tables built
once, Map for living collections. (An earlier draft of this page
claimed a per-host accent palette type here; no such type exists in
the shipping sources, so the claim is gone. Verify, then trust.)
New here: optional, readonly, index signatures, Record. Docs:
Object Types
(readonly modifiers, index signatures). Watch
121 — Utility Types
for Record in its natural habitat — Chapter 07 runs the full
utility survey, but read the Record segment now so the knot
table in the lab arrives pre-solved.
Extending and composing
Shapes grow two ways. Interfaces extend: a Hitch is a Knot with
more fields, declared once, inheriting the rest:
export interface Hitch extends Knot {
wraps: number;
locked: boolean;
}
Aliases and interfaces both compose with intersections: Hitch & Anchor
is a value satisfying both shapes at once. Prefer extends for
is-a-more-specific hierarchies (hitch is a knot), & for combining
independent capabilities (a hitch that is also an anchor, where neither
shape owns the other). Both keep one definition per concept — rename a
field on Knot and every extension and composition follows.
New here: extends, intersections. Docs:
Interfaces
(extending) and Everyday Types
(intersections). Drill 2 below is the whole lesson in four lines:
extends with a conflicting field refuses (TS2430), while the
& version compiles into a shape no value can satisfy. Extending
fails loud at the declaration; intersecting fails silent at every
use. Prefer the loud one.
Structural typing
TypeScript checks shape, not origin. Any object with the right fields satisfies the type, regardless of where it was built or what else it carries:
export function label(k: Knot): string {
return `${k.name} holds ${k.loops} loops`;
}
label({ name: "Bowline", use: "loop", material: "nylon", loops: 2 });
const hitch = {
name: "Clove hitch",
use: "hitch",
material: "nylon",
loops: 2,
wraps: 3, // extra fields ride along through a variable... see note
};
label(hitch);
The rule is “at least these fields with these types.” This is why test
doubles, parsed JSON, and hand-built fixtures slide into typed functions
without ceremony: if it has the shape, it is the shape. One sharp edge to
memorize: a fresh object literal passed directly is checked for excess
fields (a typo like jersy: 12 is caught), while the same object through a
variable is not — the checker assumes a stored value may legitimately carry
more. Excess-property checks catch misspellings at the call; structural
acceptance keeps composition painless everywhere else.
New here: structural typing, excess-property checks. Docs: Object Types (excess property checks) and Type Compatibility. Drill 4 is the rule in three calls: fresh literal with extras refuses, stored variable with extras passes, missing required field always refuses. “At least these fields with these types” — the sentence the whole chapter hangs on.
DIY simulator drills
You are the simulator. Cover the answers, predict, then verify.
Log each miss in one sentence. Run one drill per file — the drills
reuse the name Knot, and same-named interfaces in one program merge
into a single shape (declaration merging).
That merge is compiler behavior, not a drill artifact; keep the drills
apart so you test what each one claims.
Be the checker. Under
strict: true, which lines error?interface Knot { name: string; loops?: number; readonly material: string; } const a: Knot = { name: "Bowline", material: "nylon" }; const b: Knot = { name: "Bowline", material: "nylon", jersy: 12 }; const c = { name: "Bowline", material: "nylon", jersy: 12 }; const d: Knot = c; a.material = "poly";Verify with
npx tsc --noEmit. (Thejersyline refuses, the lookalike through a variable passes, and one more line refuses for an entirely different reason. Name all three mechanisms.)Be the extender. Which declarations compile?
interface Knot { name: string; loops: number; } interface Hitch extends Knot { loops: string; } type Anchor = Knot & { loops: string };Verify with
npx tsc --noEmit. (Extending narrows nothing — a conflicting field is a flat refusal, while the intersection compiles into something no value can ever satisfy. Say which is which, and which failure you would rather debug.)Be the indexer. Which table compiles, and which lies?
type Crew = "aiko" | "zola"; const t1: Record<Crew, number> = { aiko: 3 }; const t2: { [name: string]: number } = { aiko: 3 }; t2.zola = 5;Verify with
npx tsc --noEmit, then run both tables innode(strip the types) and readt2. (The closed table refuses an absent member at compile time; the open one accepts everything and proves nothing. Match each to its production cousin from this chapter:Recordfor fixed tables,Mapfor living collections.)Be the structuralist. What does
labelaccept?interface Knot { name: string; loops: number; } function label(k: Knot): string { return `${k.name}: ${k.loops}`; } label({ name: "Hitch", loops: 2, wraps: 3 }); const h = { name: "Hitch", loops: 2, wraps: 3 }; label(h); label({ name: "Hitch" });Verify with
npx tsc --noEmit. (Fresh literal, stored variable, missing field — three receptions for three shapes. State the rule in one sentence before reading on.)
Lab: model the knot domain
The domain-model kit gives you a flat knot list of loosely typed rows
and a red suite describing the domain: a Knot interface with
readonly fields, an optional repair note, a use literal union, a
Record<Use, string[]> knot table, and a lookup whose misses return
undefined. Write the Knot interface, implement the label, the lookup,
and the table, and watch the suite go green. The final test passes a
hitch object carrying an extra slips flag into label(k: Knot) —
it passes because structure decides and origin does not matter.
Resources
Docs, in the order the chapter introduces them:
- Object Types (shapes, modifiers, index signatures, excess checks)
- Interfaces (declaring, extending)
- Everyday Types (aliases, intersections)
- Type Compatibility (structure, not origin)
- Declaration Merging (why drills run one per file)
Videos — the chapter’s shelf, section-mapped (quoted lines are the instructor’s own words as preserved in each companion entry):
- Shapes and contracts: 015 — Interfaces · 093 — Interfaces in Practice (“set that type to be IsPerson”) · 104 — Type vs Interface (“completely backwards because interfaces are more complicated than types”)
- Collections and tables: 083 — Objects and Arrays · 005 — Objects and Arrays · 121 — Utility Types (
Recordsegment) - Full-hour companion: 079 — TypeScript for Beginners, Part B
Hands-on: drill set above (one file per drill, predict first,
tools second), then the domain-model kit — Knot interface,
lookup with undefined misses, Record<Use, string[]> table, and
the hitch-through-label finale.