154: TypeScript - Narrowing and Domain Models
Learning outcomes
- use unions, literal types, predicates, and discriminated unions;
- narrow
unknownsafely; - model valid task states without ambiguous combinations.
Study
Types should make invalid states difficult to represent, not merely decorate JavaScript. Prefer TaskStatus = "open" | "done" over arbitrary strings. Narrow external data with checks or a schema parser. A type assertion changes the compiler's belief; it does not inspect the value.
Practice
Implement parseTask(value: unknown): Task with checks for an ID, title, status, and timestamps. Return a success or validation-failure union. Test missing fields, extra fields, and wrong runtime types.
Worked example
Narrow one field at a time. Do not use Object.keys(value) as proof that the values have the right types.
type Task = { id: string; title: string; status: 'open' | 'done' }
type ParseResult = { ok: true; value: Task } | { ok: false; message: string }
function parseTask(value: unknown): ParseResult {
if (typeof value !== 'object' || value === null) return { ok: false, message: 'object required' }
const record = value as Record<string, unknown>
if (typeof record.id !== 'string' || typeof record.title !== 'string') {
return { ok: false, message: 'id and title required' }
}
if (record.status !== 'open' && record.status !== 'done') {
return { ok: false, message: 'invalid status' }
}
return { ok: true, value: { id: record.id, title: record.title, status: record.status } }
}
The assertion is local and immediately followed by checks. It does not make the whole input trusted.
Edge cases and questions
Test null, arrays, inherited properties, empty strings, a numeric status, and an object with extra fields. Explain why a discriminated result forces the caller to handle failure before using value.
Checkpoint
Explain the difference between a union, intersection, type predicate, and discriminated union. Demonstrate that malformed JSON cannot become a Task by assertion alone.
