unrust

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.

A TypeScript boundary & narrowing review table
Observed conditionReview decisionWhat to verify
External data (req.json(), JSON.parse, env vars) is typed using an `as SomeType` assertionReplace bare `as` assertions at I/O boundaries with runtime schema parsing or explicit field guardsMalformed 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 bodyAn 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?

Consider this TypeScript helper: function applyDiscount(priceCents: number, discountPct?: number): number { if (!discountPct) { return priceCents; } return Math.round(priceCents * (1 - discountPct / 100)); } This function works for 0% and 20%, but a developer refactors it to support a price override: function resolvePrice(baseCents: number, overrideCents?: number): number { return overrideCents ? overrideCents : baseCents; } What breaks when a caller passes resolvePrice(1500, 0) for a free promotional item?
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

  1. 01Audit every `as` type assertion in a diff and ask whether the value just crossed an unvalidated runtime I/O boundary.
  2. 02Test every union narrowing check with 0, false, and '' to verify valid boundary values are not dropped.
  3. 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.

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