Checkpoint: Type-Level Patterns
This checkpoint consolidates the type-level toolkit — assertions, any/unknown/never, structural typing, type vs interface, readonly, Records, template-literal and recursive types, satisfies, and branded types — into one build challenge and a short quiz.
Learn Checkpoint: Type-Level Patterns in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
1. Recap: The Type-Level Toolkit
Here's the whole chapter at a glance — skim it, and re-open any lesson whose one-liner doesn't click yet.
2. 🎯 Build Challenge: A Tiny Typed Event System
Put it together. Build a small event system that uses a branded validated id, a Record handler registry, an exhaustive dispatch with a never -style default guard, and an immutable frozen config. Complete the TODOs in the starter below and run it; then compare with the full solution.
A complete, runnable version. Each numbered step maps to a lesson from the recap.
Expected output: evt_1 , created user , deleted user , retries=3 , bad event id: x_1 , unhandled kind: updated .
3. 📝 Checkpoint Quiz
Answer each in your head, then expand to check. If one trips you up, revisit its lesson from the recap.
No. A type assertion only changes what the compiler believes — it's erased at runtime. The value is still the string "42" , so "42" as number followed by + 1 would concatenate to "421" . For a real conversion, use Number("42") .
Use unknown for any value whose type you don't control — parsed JSON, network data, callback inputs. Unlike any (which disables checking and spreads), unknown forces you to narrow with typeof , instanceof , or a type guard before you can use the value, keeping you safe.
Because TypeScript is structurally typed: compatibility depends on shape, not name. Both types have a name: string member, so each is assignable to a parameter typed {' '} — the extra properties are simply ignored. Their unrelated names don't matter.
as T is an assertion: it forces the type with no validation, so mistakes pass silently. satisfies T actually checks the value against T (reporting mismatches) while preserving the precise inferred type — no widening. Prefer satisfies when shaping a literal you want validated.
Both are strings, so structurally they'd be interchangeable. A brand intersects each with a distinct phantom property ( {'& '} vs ), giving them different types. The compiler then refuses to pass one where the other is required. The brand is type-only and erased at runtime.
Erased (compile-time only): as , ! , any / unknown / never , readonly , as const , template-literal types, recursive types, satisfies , and brands. Real runtime behaviour comes from the JavaScript you pair them with: Object.freeze (immutability), typeof / Array.isArray (narrowing), and validation inside smart constructors.
Practice quiz
Does the assertion '"42" as number' turn the value into a number at runtime?
- Yes, it parses the string into 42
- Yes, but only with strict mode on
- No — assertions are erased; the value is still the string '42'
- No, it throws a runtime error
Answer: No — assertions are erased; the value is still the string '42'. A type assertion only changes what the compiler believes; it is erased at runtime. Use Number('42') for a real conversion.
When should you prefer 'unknown' over 'any'?
- For values whose type you don't control — unknown forces you to narrow before use
- Never; they behave identically
- Only for numbers
- When you want to disable type checking
Answer: For values whose type you don't control — unknown forces you to narrow before use. Unlike 'any', which disables checking, 'unknown' forces narrowing (typeof, instanceof, a guard) before the value can be used — keeping you safe.
What is the 'never' type used for?
- Any value at all
- Null and undefined together
- A function with no parameters
- A value that can never occur — used for exhaustiveness checks
Answer: A value that can never occur — used for exhaustiveness checks. 'never' represents the impossible. In a switch default, assigning the discriminant to 'never' flags any unhandled case at compile time.
Why can both a Person and a Product satisfy a parameter typed '{ name: string }'?
- Because they share a base class
- Because TypeScript is structurally typed — shape, not name, decides compatibility
- Because all objects have a name
- Because of declaration merging
Answer: Because TypeScript is structurally typed — shape, not name, decides compatibility. TypeScript uses structural typing: any object whose shape includes 'name: string' is assignable; extra properties are ignored and names don't matter.
Which statement about 'type' aliases vs 'interface' is correct?
- Interfaces merge across declarations and extend; type aliases name unions, tuples, and mapped types
- Interfaces can name unions; type aliases cannot
- They are completely interchangeable in all cases
- Type aliases can be re-declared to merge members
Answer: Interfaces merge across declarations and extend; type aliases name unions, tuples, and mapped types. Interfaces support declaration merging and 'extends'; type aliases are closed but can express unions, tuples, and mapped types that interfaces can't.
What is the difference between 'value as T' and 'value satisfies T'?
- They are identical
- 'satisfies T' converts the value; 'as T' validates it
- 'as T' forces the type with no validation; 'satisfies T' checks the value against T while keeping the precise inferred type
- 'satisfies T' is erased but 'as T' runs at runtime
Answer: 'as T' forces the type with no validation; 'satisfies T' checks the value against T while keeping the precise inferred type. 'as' is an unchecked assertion. 'satisfies' validates the value against T and reports mismatches while preserving the narrow inferred type (no widening).
How do branded types stop you mixing a UserId and an OrderId, given both are strings?
- They store a hidden runtime tag on each value
- Each is intersected with a distinct phantom property, giving them different (nominal) types
- They convert the strings to numbers
- They use Object.freeze to lock the value
Answer: Each is intersected with a distinct phantom property, giving them different (nominal) types. Brands intersect each string with a distinct phantom property (e.g. & { __brand: 'UserId' }), so the compiler refuses to swap them. The brand is type-only and erased.
What does a template literal type let you express?
- A runtime string built with backticks
- A multi-line comment
- A recursive function
- String types built from patterns, such as 'on${Capitalize<string>}'
Answer: String types built from patterns, such as 'on${Capitalize<string>}'. Template literal types compose string types from patterns and unions distribute over them; helpers like Capitalize transform the pieces. They are compile-time only.
What characterizes a recursive type?
- A type that calls a function
- A self-referential type, like a JSON value or tree, that refers to itself in its own definition
- A type that can only be used once
- A type alias for 'any'
Answer: A self-referential type, like a JSON value or tree, that refers to itself in its own definition. Recursive types refer to themselves to model nested structures (JSON, trees, linked lists), e.g. DeepReadonly<T> applying itself to nested properties.
Which of these has REAL runtime behaviour rather than being erased?
- The 'readonly' modifier
- A branded type
- Object.freeze
- The 'satisfies' operator
Answer: Object.freeze. Object.freeze actually enforces immutability at runtime. 'readonly', 'satisfies', and brands are compile-time only and leave no trace in emitted JavaScript.