TypeScript Narrowing: Validate Values Before Using Them
TypeScript narrowing connects runtime decisions with compile-time types. A value that starts as a union becomes more specific after a check the compiler understands. This matters when reading form fields, handling optional values, or consuming an API response. The type system helps you describe what is possible, but it does not inspect a network response for you. Learning to narrow values keeps the code honest about the difference between a declared type and data received from another system.
TypeScript narrowing: a practical workflow
The sequence is Unknown input → Runtime check → Narrowed type → Safe operation. The sections below explain what each decision changes, which failure cases deserve attention, and how to verify the result in a small project. Use the example as a starting point, then adapt the interfaces and error policy to your own application.
Start with unknown at external boundaries

Treat untrusted data as unknown until a runtime check establishes its shape. Unlike any, unknown prevents property access and method calls before validation. A TypeScript interface describes the shape you expect; it does not transform or validate JSON. If the payload changes, a type assertion can silently hide the mismatch. Keep parsing close to the boundary and return a clearly typed result from that parser. Downstream functions can then work with checked data instead of repeating defensive checks throughout the application.
Use guards that match the operation
Use typeof for primitive values, Array.isArray for arrays, and explicit null checks before reading objects. Remember that typeof null is object, so checking only typeof value === "object" is insufficient. Truthiness is useful when empty values are intentionally excluded, but it also excludes zero and an empty string. For optional numeric input, a check against undefined is usually clearer. The guard should express the business rule as well as satisfy the compiler; these are related responsibilities, not interchangeable ones.
Model alternatives with discriminated unions

A shared literal property makes alternative states easy to distinguish. For example, a result can have kind equal to success with a value, or kind equal to failure with a message. Checking kind narrows the whole object, so the failure branch cannot accidentally read a success value. This is especially useful for UI loading states and service outcomes. Avoid a single object with many optional properties when only certain combinations make sense. A union records those relationships directly and makes invalid combinations harder to construct.
Keep user-defined predicates trustworthy
A predicate returning value is SomeType is a promise about its implementation. TypeScript trusts that promise, even when the function checks too little. Check every property required by the declared result or use a runtime schema library for complex payloads. Assertions should be reserved for cases where you can explain the independent evidence for the type. They are not a substitute for validation. If a parser rejects input, return a useful error without exposing sensitive payloads in the message.
Example and implementation notes
type Result = { kind: "ok"; value: number }
| { kind: "error"; message: string };
function render(result: Result): string {
if (result.kind === "ok") return String(result.value);
return result.message;
}
This example illustrates the core contract rather than a complete production application. Connect it to your real inputs, authentication rules, and failure-handling paths before using it. Check the expected output first, then change one input at a time to verify the assumptions. Keep those checks alongside the implementation so future changes do not silently weaken the behavior.

Verification checklist
- Compile both branches with strict mode enabled.
- Pass null, an empty string, zero, and an unexpected object to your boundary parser.
- Add a new union member and inspect which consumers need updating.
- Check that invalid network data produces an explicit error.
Practice exercise
Build a parser for a settings response containing a theme string and an optional page-size number. Reject themes outside the supported set and reject non-integer page sizes. Accept zero only if your product defines a meaning for it; otherwise return a validation error. Test missing properties, null, strings where numbers are expected, and a valid payload. Your success criterion is that the UI never receives an unchecked value and that each failure gives the caller a usable reason.

Review the result
After completing the exercise, describe the successful path and one failure path in plain language. Record the input, expected result, actual result, and the evidence you used to compare them. A passing demonstration should remain repeatable after a dependency update or a refactor. If you cannot reproduce the result, reduce the example until the source of uncertainty is visible. Keep the smallest useful regression check rather than depending on memory.
References and related reading
Consult the official reference for TypeScript narrowing for exact platform behavior. Continue with a practical debugging workflow and unit testing basics to turn these decisions into repeatable checks.
Discussion
Join the conversation