The compiler and runtime
Validate data at runtime
Treat JSON, environment variables, form fields, and network responses as unknown until code has checked their actual shape.
Types are erased before your program runs, so nothing checks the data that arrives from outside. JSON.parse() returns whatever the text contained, no matter what type you wish it had. The honest move is to start uncertain input as unknown. This forces you to prove a useful shape before accessing fields.
Write the proof as a type predicate:
type Task = { id: number; title: string }
function isTask(value: unknown): value is Task {
if (typeof value !== 'object' || value === null) return false
if (!('id' in value) || !('title' in value)) return false
return typeof value.id === 'number' && typeof value.title === 'string'
}
The checks build on each other. First rule out non-objects and null — remember typeof null is 'object', so the explicit null check is not optional. Then confirm the properties exist. Only then is it safe to inspect their types.
Use the guard at the boundary:
const input: unknown = JSON.parse(text)
if (!isTask(input)) {
throw new Error('Invalid task data')
}
console.log(input.title)
After the guard, input is a Task, and the property access compiles. Before it, input.title fails with 'input' is of type 'unknown'. — the compiler holding the line until you validate.
Test it against hostile input: isTask(null), isTask([]), and isTask({ id: '7', title: 'Ship' }) all return false. The string '7' is the case annotations alone would never catch.
The runtime check protects the running program. The type predicate describes what is true after validation. Neither one replaces the other.
Where to validate
Keep validation close to JSON, network, form, storage, and environment boundaries. Validate once, as the data enters, then pass the trusted Task inward. The rest of the application can then work with trusted values and never repeat the checks.
Hand-written guards scale poorly past a few fields. For larger contracts, a schema library can produce consistent errors and inferred types from one definition. Still test missing fields, wrong types, null, arrays, and oversized values — the library runs your schema, and the schema can be wrong.
Exercise: add a boolean completed field and verify that the guard rejects the string 'false'.
Lesson completed