TypeScript review guide
How do you verify TypeScript type narrowing at runtime boundaries?
Published
Published by Unrust. Scoring and editorial method
Quick answer
To verify TypeScript type narrowing at runtime boundaries, treat every external input (JSON.parse, req.body, URL query params, database JSONB, and environment variables) as unknown until a runtime check verifies its shape, and audit every type guard or conditional check to ensure truthiness (if (val)) does not accidentally exclude valid 0, false, or empty-string values.
Compile-time types vanish at runtime I/O boundaries
Writing const payload = (await req.json()) as InvoiceRequest silences the TypeScript compiler without running a single runtime check. If a client sends { amount: '50' } as a string instead of a number, payload.amount + 10 evaluates to '5010' at runtime.
Whenever code crosses an I/O seam—HTTP bodies, webhook payloads, localStorage, or message queues—verify that a runtime validator or explicit typeof / Array.isArray guard checks the value before the rest of the module trusts the narrowed type.
Watch out for truthiness narrowing on union types
In TypeScript, if (value) narrows string | number | null | undefined by removing null and undefined—but it also skips the branch when value is 0, '', or false.
When reviewing code that handles numeric offsets, prices, retry counts, or boolean flags, check that narrowing uses value !== null && value !== undefined (or value != null) rather than bare truthiness.
Audit custom user-defined type predicates (x is T)
A function returning val is AdminUser tells the compiler to trust whatever boolean expression the function returns. If the predicate only checks 'role' in val without checking that val.role === 'admin', TypeScript still narrows the type to AdminUser on the true branch.
Read the body of every custom x is T helper with a malicious or malformed object in mind to confirm the runtime check is as strict as the narrowed type claims.
A TypeScript boundary & narrowing review table
Use this checklist when reviewing TypeScript pull requests that parse external data or narrow union types.
| Observed condition | Review decision | What to verify |
|---|---|---|
| External data (req.json(), JSON.parse, env vars) is typed using an `as SomeType` assertion | Replace bare `as` assertions at I/O boundaries with runtime schema parsing or explicit field guards | Malformed or stringified fields are rejected or coerced predictably at runtime instead of corrupting downstream math |
| A union type containing number, boolean, or string is narrowed with `if (x)` or `x ? a : b` | Trace what happens when x is 0, false, or '' | Explicit `x !== undefined` or `x ?? default` is used when 0, false, or '' is a valid domain value |
| A helper uses a custom type predicate (`function isValid(x: unknown): x is Target`) | Check every required property and literal discriminant of `Target` inside the predicate body | An object with `{ role: 'guest' }` or `{ amount: NaN }` cannot pass a predicate that narrows to a privileged or numeric type |
Interactive reasoning check
Try the TypeScript review: why does the discount check fail?
Read the reasoning without answering
TypeScript's type narrowing allows if (overrideCents) to narrow number | undefined to number, but at runtime that check also excludes 0. Use overrideCents ?? baseCents or overrideCents !== undefined.
A small practice loop
- 01Audit every `as` type assertion in a diff and ask whether the value just crossed an unvalidated runtime I/O boundary.
- 02Test every union narrowing check with 0, false, and '' to verify valid boundary values are not dropped.
- 03Use the free JavaScript & TypeScript Truthiness Explorer at /tools/javascript-truthiness to compare ||, ??, and explicit undefined checks.
Common questions
Does enabling strictNullChecks in tsconfig.json prevent falsy 0 or false bugs?
No. strictNullChecks forces you to handle null and undefined before using a value, but if you handle them with `if (val)` or `val || default`, runtime truthiness will still catch 0, false, and ''.
Where can I try a hands-on TypeScript boolean-default challenge?
Open the TypeScript explicit-false challenge at /guides/typescript-explicit-false-challenge or take the free TypeScript diagnostic at /diagnostic.
Limits of this guide
Runtime boundary checks protect data integrity at module seams; they do not replace compile-time strictness flags such as strictNullChecks or noUncheckedIndexedAccess.
Sources and scope
Original editorial guide by the Unrust team covering TypeScript type narrowing, type predicates, and runtime boundary validation.
- TypeScript Handbook: Narrowing and Truthiness Guards
Microsoft TypeScript Documentation
Explains how truthiness narrowing interacts with falsy primitives like 0, '', and false.
- OWASP Input Validation Cheat Sheet
OWASP Foundation
Covers why runtime validation is required at trust and serialization boundaries regardless of static source types.
Check your own starting point
Unrust uses short, timed, unassisted drills to give developers a per-skill snapshot. One session cannot explain the cause of a result, but it can show you what is worth practicing next.
Take the free diagnostic