Type narrowing is how TypeScript figures out the specific type of a value inside a conditional branch. Every time you write if (response.ok) or if (typeof input === "string"), the compiler tracks what types are still possible and gives you access to the right properties. It is the mechanism that makes union types practical instead of painful.
Master typescript type narrowing
Take the TypeScript Essentials course with hands-on lessons and challenges.
TypeScript narrows each branch of the union based on which property is present. Checking result.error eliminates the success variant, and the else branch eliminates the error variant. This pattern replaces try/catch for expected failures.
The return type `input is CreateOrderPayload` is a type predicate. It tells TypeScript that when this function returns true, the argument is safe to use as CreateOrderPayload. This is the standard pattern for validating untyped data at system boundaries — API handlers, webhook receivers, message queue consumers.
The type field is the discriminant — its literal value tells TypeScript exactly which variant you are dealing with in each case branch. The never assignment in the default case is the exhaustiveness check: if you add a new payment method like { type: "crypto" } without handling it, the compiler flags the error immediately.
Order matters with instanceof: check the most specific subclass first. If you checked HttpException before ValidationException, the ValidationException branch would never execute because ValidationException extends HttpException. TypeScript follows the same narrowing logic the runtime does.
The in operator checks for property existence at runtime and narrows the type accordingly. This is the go-to narrowing technique for plain objects from external sources (API responses, config files, deserialized JSON) where instanceof is not an option because there are no class instances involved.
Assertion functions use the asserts keyword in their return type. Unlike type predicates (which return a boolean), assertion functions throw if the condition is not met — and TypeScript narrows the type for all subsequent code in the same scope. This is cleaner than repeating if-throw blocks for required values.
Using a type assertion (`as Type`) instead of a runtime narrowing check — e.g., `(req.body as CreateOrderPayload).productId` without validating first
Write a type guard function or an inline check. Assertions tell the compiler to trust you, but they generate zero runtime code. If the data does not match the expected shape, you get a crash. Narrowing with `typeof`, `in`, or a custom type predicate actually verifies the data.
Checking `typeof value === "object"` without also checking `value !== null` — since JavaScript's `typeof null` returns `"object"`
Always guard against null explicitly: `if (value !== null && typeof value === "object")`. This is a JavaScript quirk from 1995 that will never be fixed, so it needs to be handled every time.
Checking `instanceof` against a subclass after already matching the parent class — the subclass branch becomes dead code
Order instanceof checks from most specific to least specific. Check `ValidationException` before `HttpException`, check `HTMLInputElement` before `HTMLElement`. The first matching branch wins.
Using truthiness narrowing (`if (value)`) when the value could legitimately be `0`, `""`, or `false` — these are valid values that fail truthiness checks
Use explicit comparisons: `if (value !== null && value !== undefined)` or `if (value != null)` (loose equality). The loose `!= null` check eliminates both null and undefined while keeping 0, empty string, and false.
Type narrowing is TypeScript's ability to refine a broad type to a specific one based on runtime checks. The compiler tracks `typeof`, `instanceof`, `in`, equality, and truthiness checks through your control flow and adjusts the type accordingly. For custom logic, type predicates (`param is Type`) and assertion functions (`asserts param is Type`) extend narrowing across function boundaries. Discriminated unions with exhaustiveness checking give you compiler-enforced coverage of every variant. The core principle: narrow with runtime checks, not type assertions.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.