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.
What You'll Practise
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.
- 🪪 Type Assertions — as re-labels a type (no runtime conversion); ! drops null/undefined; as unknown as T is the escape hatch.
- 🎭 any / unknown / never — any disables checks; unknown forces narrowing; never is impossible (exhaustiveness).
- 🕳️ void / null / undefined — no-return type; absent vs intentionally-empty; ?. and ?? .
- 🦆 Structural Typing — shape, not name, decides compatibility; excess-property checks on literals.
- 🆚 type vs interface — interfaces merge/extend; type aliases name unions/tuples/mapped types.
- 🔒 readonly / as const — compile-time immutability; Object.freeze for runtime (shallow).
- 🗂️ Records / Index Signatures — dictionaries; Record<K,V> ; noUncheckedIndexedAccess .
- 🧩 Template Literal Types — string types from patterns; unions distribute; Capitalize & friends.
- 🌳 Recursive Types — self-referential types (JSON, trees, lists); DeepReadonly<T> ; depth limits.
- ✅ satisfies — validate without widening; keep precise inference.
- 🏷️ Branded Types — phantom brands for nominal distinctions; smart constructors.
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 { name: string } — 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 ( & { __brand: "UserId" } vs "OrderId" ), 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.
📚 Keep learning
- TypeScript Handbook — Everyday Types
- TypeScript Handbook — Utility Types
- TypeScript Handbook — Declaration Merging (up next)
Frequently Asked Questions
🎉 Checkpoint Complete
- ✅ You can name and reach for each type-level tool deliberately
- ✅ You combined brands, Records, exhaustive dispatch, and frozen config
- ✅ You know which features are erased and which run at runtime
- ✅ You validated-then-branded with a smart constructor
- ✅ You self-checked with the quiz — revisit any topic that wobbled
- ✅ Next lesson: Declaration Merging — combining and augmenting types
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.
Continue this course
- Previous: Branded Types
- Next: Declaration Merging