TypeScript Foundations: Narrow Unknown Data Before Trusting It
Distinguish compile-time guarantees from runtime data and use narrowing, discriminated unions, and explicit parsing at service boundaries.
On this page
Key takeaways
- TypeScript types are erased; a type assertion does not validate JSON.
- Accept unknown at untrusted boundaries, then parse it into a domain type.
- Discriminated unions make valid states and exhaustive handling visible.
Types are a development-time contract
TypeScript checks relationships between values in your program before execution. Its annotations do not become runtime validators. A network response cast with as Order can still contain a missing ID, a string where a number was expected, or an unexpected status. The assertion changes the compiler's belief, not the data.
Use unknown for input whose shape has not been established: parsed JSON, a message from another process, or a value restored from browser storage. Narrow it with checks or a maintained schema parser. After parsing succeeds, pass the resulting domain value to code that can rely on its invariants. Keep validation errors separate from unexpected runtime failures so a bad payload is not reported as a server crash.
Narrow properties together
Checking that a value is an object is only the first step. JavaScript considers null an object, arrays are objects, and property existence does not prove the property's type. A reusable parser should check these cases and any domain constraints, such as a positive quantity or a finite total. JSON can represent numbers outside the integer precision your application can safely use, so validate safe integer ranges for numeric identifiers.
function parseQuantity(value) {
if (!Number.isSafeInteger(value) || value < 1) {
throw new Error("Quantity must be a positive safe integer");
}
return value;
}
console.log(parseQuantity(3));
This JavaScript example illustrates a runtime check. In TypeScript, the validated result can have a number return type without an assertion. Keep parsing at the boundary instead of repeating partial checks throughout business logic.
Describe states with discriminated unions
Avoid independent flags such as loading, error, and dataAvailable when combinations can be contradictory. A union with states idle, loading, success, and failure expresses which properties belong together. A success branch carries data; a failure branch carries an error; a loading branch does not pretend to have either.
Use an exhaustive switch and a never check where adding a new state should require updating every consumer. Do not suppress missing cases with a broad default branch that returns an arbitrary success value. Exhaustiveness is especially valuable for protocol messages and background jobs, where a forgotten state otherwise becomes a silent operational failure.
Make absence explicit
Optional properties and nullable values represent different contracts. An absent PATCH field can mean "leave unchanged", while null may mean "clear this value". Preserve that distinction through parsing and persistence. Enable strict checking in the project and avoid any as a shortcut across a boundary.
Test the parser with null, arrays, missing fields, wrong primitive types, excessive sizes, and non-finite values from non-JSON sources. Also test valid values: a parser that rejects every input is safe from crashes but is not a working contract. Compiler checks and runtime tests protect different failure classes; a reliable TypeScript service uses both.