Runtime-Validated Data Boundary and Typed Event Hub
Practical 05 - Runtime-Validated Data Boundary and Typed Event Hub
Related: Chapter 5 · Lecture slides
Objective
Build a resilient API trust boundary in TypeScript that ingests unknown external data, enforces runtime validation before types are asserted, transforms verified input into trusted domain records, and surfaces user-friendly diagnostics when contracts fail.
You will test your boundary against a rigorous matrix of valid, malformed, incomplete, and unexpected payloads. Furthermore, you will connect this laboratory to Chapter 4 by upgrading the pure JavaScript event hub with compile-time generic event maps.
Prerequisites and setup
You need Node.js (v18+) and the TypeScript compiler (tsc).
Create your exercise workspace:
Ensure your tsconfig.json enforces full strictness:
Do not use as SomeType type assertions to bypass validation. Every domain record must be proven at runtime before it can enter application state.
Stage 1 - Model domain records and discriminated results
In src/types.ts, define your domain models using discriminated unions:
Stage 2 - Implement the runtime parser and test matrix
In src/boundary.ts, implement a validation parser parseServiceRecord(raw: unknown): Result<ServiceRecord, BoundaryError>. You may implement this with explicit property checks or with a schema parsing library (such as Zod).
Execute the parser against a four-case test matrix:
- Valid Case:Expected: Returns
{ ok: true, data: ServiceRecord }. - Malformed Case:Expected: Fails schema validation with specific messages indicating
feeIqdmust be a number andisAvailablemust be a boolean. - Incomplete Case:Expected: Fails schema validation reporting missing required fields (
name,feeIqd,isAvailable). - Unexpected Backend Case:Expected: Correctly identified as an incompatible schema without crashing with a
TypeError.
Verify: Run tsc --noEmit. Verify that result.data is completely inaccessible on { ok: false } branches, and that narrowing on result.ok permits safe access to all domain fields.
Stage 3 - Distinguish transport failures from schema failures and surface to UI
In src/app.ts, coordinate network fetching, validation, and DOM updates:
- Differentiate network disconnects and HTTP 500 errors (
kind: 'transport') from schema decoding errors (kind: 'schema'). - Surface appropriate feedback to the user:
- Transport Errors: Inform the user: “Network connection unavailable. Please check your connection and retry.”
- Schema Errors: Inform the user: “Service record received in an unrecognized format. Our technical team has been alerted.” Log detailed structural issues to the console without exposing sensitive internals to the user interface.
Verify: Trigger each error condition in the browser UI. Verify that error messages are rendered inside an accessible container (role="alert" or aria-live="polite").
Stage 4 (Chapter 4 Extension) - Strongly typed event hub
Extend the publish-subscribe event hub from Practical 04 with a compile-time generic contract:
Verify: In src/app.ts, instantiate createTypedEventHub<CitizenEventMap>().
- Verify that
hub.emit('search:start', { query: 'res', timestamp: Date.now() })compiles cleanly. - Verify that
hub.emit('invalid:channel', {})fails compilation with an invalid event name error. - Verify that
hub.emit('search:start', { query: 123 })fails compilation due to mismatched payload shape.
What to submit
Submit your TypeScript source files (src/types.ts, src/boundary.ts, src/event-hub.ts, src/app.ts) and a completed verification report:
| Target | Requirement | Observed Evidence | Explanation | Confounders or Limits |
|---|---|---|---|---|
| Type Erasure Awareness | Zero as assertions on external JSON | Code audit of boundary.ts | All JSON parsed through runtime validator | Checked via compiler |
| Payload Matrix | All 4 test cases handled predictably | Test runner output | Valid parsed; malformed/incomplete/error rejected | Tested in test harness |
| Error Differentiation | Transport vs schema errors separated | UI error snapshot | User receives actionable context; diagnostics logged | Network throttled in DevTools |
| Typed Event Hub | Compile-time check on events & payloads | tsc --noEmit failure test | Invalid event names and payloads rejected by tsc | Verified against EventMap |
When an experiment gives an unexpected result
- TypeScript permits reading invalid properties: Ensure you did not cast the fetch result as
anyor useas ServiceRecord. Input must remainunknownuntil narrowed. BoundaryErrorinstanceof check fails: When compiling TypeScript to older targets (ES5), subclassingErrorcan break prototype chains. Ensure"target": "ES2022"is configured intsconfig.json.- Optional properties become undefined: Remember that
noUncheckedIndexedAccess: truerequires checking whether array items or dynamic dictionary keys are defined before accessing their properties.