TypeScript

TypeScript Type Narrowing👨‍💻

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.

Key Takeaways

  • 1TypeScript's control flow analysis automatically narrows types after `typeof`, `instanceof`, `in`, equality checks, and truthiness checks — you don't need to do anything special
  • 2`typeof` handles primitives (`string`, `number`, `boolean`, `undefined`), but watch out: `typeof null` returns `"object"`, not `"null"`
  • 3`instanceof` narrows class instances — use it for `Error` subclasses, DOM elements, and your own class hierarchies
  • 4The `in` operator narrows based on property existence, which is ideal for plain objects coming from API responses where you can't use `instanceof`
  • 5User-defined type guards (`param is Type`) let you extract complex narrowing logic into reusable functions that TypeScript trusts across call boundaries
  • 6Discriminated unions (a shared literal `type` or `kind` field) combined with `switch` statements give you exhaustive, compiler-verified handling of every variant

Master typescript type narrowing

Take the TypeScript Essentials course with hands-on lessons and challenges.

Examples

Narrowing API responses with typeof and truthiness

typescript

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.

Custom type guard for validating unknown input

typescript

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.

Discriminated union for payment processing

typescript

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.

instanceof for error handling middleware

typescript

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 for narrowing API response shapes

typescript

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.

Narrowing with assertion functions (TypeScript 3.7+)

typescript

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.

Common Mistakes

Mistake:

Using a type assertion (`as Type`) instead of a runtime narrowing check — e.g., `(req.body as CreateOrderPayload).productId` without validating first

Fix:

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.

Mistake:

Checking `typeof value === "object"` without also checking `value !== null` — since JavaScript's `typeof null` returns `"object"`

Fix:

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.

Mistake:

Checking `instanceof` against a subclass after already matching the parent class — the subclass branch becomes dead code

Fix:

Order instanceof checks from most specific to least specific. Check `ValidationException` before `HttpException`, check `HTMLInputElement` before `HTMLElement`. The first matching branch wins.

Mistake:

Using truthiness narrowing (`if (value)`) when the value could legitimately be `0`, `""`, or `false` — these are valid values that fail truthiness checks

Fix:

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.

Best Practices

  • Prefer narrowing over type assertions — a runtime check protects you at both compile time and runtime, while `as` only protects at compile time
  • Extract complex narrowing logic into named type guard functions (`value is Type`) — they are reusable, testable, and make call sites cleaner than inline checks
  • Use discriminated unions with a `type` or `kind` field for any domain model that has distinct variants — then switch on the discriminant and add a `never` exhaustiveness check in the default case
  • Use assertion functions (`asserts value is Type`) for preconditions at the top of functions — they narrow the type for the entire remaining scope without nesting
  • Keep narrowing checks close to where the narrowed type is used — TypeScript's control flow analysis resets when you cross function boundaries unless you use type predicates
  • When narrowing `unknown` (e.g., from `JSON.parse` or external input), check properties incrementally rather than using a single `as` cast — each check adds a layer of safety

Summary

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.

Practice TypeScript with hands-on challenges

Learn typescript type narrowing hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master TypeScript with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.