Skip to content
Intermediate11 min study

Type narrowing

Refine unknown and union values safely with control-flow analysis, guards, predicates, discriminants, and exhaustive checks.

Question progress0 / 10 completed
Start the lesson
Type narrowing visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain type narrowing in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Overview

Narrowing is TypeScript's control-flow analysis refining a broader type to a more specific one within a branch — after an if, switch, or early return — so the compiler and the runtime agree on what's actually possible at that point.

02Mental model

  • typeof narrows primitives (string, number, boolean, etc.).
  • instanceof narrows class instances.
  • in narrows on property presence.
  • A discriminated union (a shared literal 'kind'/'type' field) narrows cleanly with a switch.
  • A custom type guard (a function returning value is T) narrows for checks TypeScript can't infer on its own.

03Examples

TypeScript
type Shape =  | { kind: 'circle'; radius: number }  | { kind: 'square'; side: number };
function area(shape: Shape) {  switch (shape.kind) {    case 'circle': return Math.PI * shape.radius ** 2;    case 'square': return shape.side ** 2;  }}

04Check understanding

Why prefer a discriminated union over a class hierarchy with instanceof checks here?

A discriminated union gives exhaustiveness checking with a switch — TypeScript can flag a missing case at compile time, which a chain of instanceof checks won't do automatically.

DDConcept deep dives

Deep dive 1

Control flow refines declared possibilities

A variable can be declared with a union while each program branch proves a narrower possibility. TypeScript follows assignments, returns, reachability, and recognized guards to calculate the type at each location. Narrowing never changes the runtime value; it lets the checker model evidence created by ordinary JavaScript control flow.

  • typeof handles primitive categories but typeof null is object.
  • Equality checks can narrow two values through their shared possible types.
  • Early returns remove handled variants from the remaining path.

Deep dive 2

Boundary data begins as unknown

JSON, storage, postMessage, URL parameters, and third-party libraries can provide values that do not match TypeScript declarations. Model them as unknown and establish evidence before using them. A type assertion only changes the compiler's belief; it does not parse, sanitize, or validate one byte at runtime.

  • Check null and object shape before reading properties.
  • Validate nested collections and value constraints, not only top-level field presence.
  • Return structured validation errors so callers cannot confuse invalid input with absence.
TypeScript
function isUser(value: unknown): value is User {  return typeof value === 'object' && value !== null &&    'id' in value && typeof value.id === 'string' &&    'name' in value && typeof value.name === 'string';}

The predicate is useful only because the implementation establishes the promised shape. A schema library is often clearer for large nested models.

Deep dive 3

Discriminants and never make branching exhaustive

When union members share a literal property, switching on it selects one valid member and its payload. After every member is handled, no value remains, represented by never. An assertNever default turns a future union addition into a compile-time request to update every exhaustive consumer.

  • Use a stable domain tag rather than testing incidental property combinations.
  • Optional properties can appear in both sides of an in-operator narrowing.
  • Exhaustiveness is strongest when strict checking and precise literal types are preserved.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Coding · 1 min · Question 1What is type narrowing?Open model answer

Model answer

Narrowing is TypeScript refining a value from a broader static type to a more specific type based on control-flow evidence. typeof, equality checks, in, instanceof, discriminants, and user-defined type predicates can all contribute evidence.

TypeScript
function format(x: string | number) {  if (typeof x === 'string') {    return x.trim();      // x is string here  }  return x.toFixed(2);    // x is number here}
Open question page →
Intermediate · Coding · 1 min · Question 2Why does typeof null equal object, and how should code narrow it?Open model answer

Model answer

That result is a historical JavaScript behavior. Check value !== null before treating an object-typed value as an object, because a typeof value === 'object' branch otherwise includes null.

TypeScript
function keys(x: object | null) {  if (typeof x === 'object' && x !== null) {    return Object.keys(x); // without the null check, x could be null  }  return [];}
Open question page →
Intermediate · Coding · 1 min · Question 3What is a type predicate?Open model answer

Model answer

A return type such as value is User tells TypeScript that a true result narrows the argument. The implementation must actually validate that claim; the compiler trusts it, so an incorrect predicate creates unsoundness similar to a bad assertion.

TypeScript
function isUser(v: unknown): v is User {  return typeof v === 'object' && v !== null && 'id' in v;}
if (isUser(payload)) {  payload.id; // narrowed to User}
Open question page →
Intermediate · Coding · 1 min · Question 4How do unknown and any differ?Open model answer

Model answer

unknown accepts any input but requires narrowing before use, preserving type safety at untrusted boundaries. any disables checking and spreads unsafety through expressions. Parse external data as unknown and validate it.

TypeScript
const a: any = JSON.parse(s);a.foo.bar;        // no error — unsafe
const u: unknown = JSON.parse(s);u.foo;            // Error — must narrow first
Open question page →
Advanced · Coding · 1 min · Question 5Why can narrowing be lost across a callback?Open model answer

Model answer

The callback may run later after a mutable property or captured binding changes, so TypeScript cannot always preserve earlier evidence. Copy the narrowed value to a const or redesign the data so mutation cannot invalidate the assumption.

TypeScript
if (obj.value !== null) {  setTimeout(() => obj.value.toUpperCase()); // Error — value may be null by now}
const v = obj.value;if (v !== null) {  setTimeout(() => v.toUpperCase()); // ok — const can't change}
Open question page →
Advanced · Coding · 1 min · Question 6What is exhaustive narrowing with never?Open model answer

Model answer

After handling every member of a discriminated union, the remaining value has type never. Assigning it to a never variable in the default branch makes the compiler report a missing case when the union expands.

TypeScript
function area(s: Shape) {  switch (s.kind) {    case 'circle': return Math.PI * s.r ** 2;    case 'square': return s.side ** 2;    default: {      const _exhaustive: never = s; // errors if a new Shape is added      return _exhaustive;    }  }}
Open question page →
Advanced · Conceptual · 1 min · Question 7How does the in operator narrow optional properties?Open model answer

Model answer

Members with an optional property may remain in both branches because the property can legally be present or absent. The check does not always select one member exclusively.

Open question page →
Advanced · Conceptual · 1 min · Question 8What is an assertion function?Open model answer

Model answer

A function returning asserts value is Type can terminate or throw when validation fails, allowing following code to treat the value as the asserted type.

Open question page →
Intermediate · Conceptual · 1 min · Question 9Why is truthiness narrowing sometimes dangerous?Open model answer

Model answer

It can remove valid falsy values such as an empty string or zero. Use an explicit nullish or domain check when those values are meaningful.

Open question page →
Advanced · Conceptual · 1 min · Question 10How does instanceof narrow values?Open model answer

Model answer

It checks the constructor's runtime instance semantics and narrows to its instance type. Cross-realm objects and customized Symbol.hasInstance behavior can make assumptions fragile.

Open question page →

SCScenario questions

Scenario 1

An API parser casts response.json() as User and production crashes on a missing field.

  1. Treat decoded JSON as unknown.
  2. Validate required fields and nested shapes at the boundary.
  3. Return a typed success/error result.
  4. Keep the validated type separate from the transport payload.
Reveal worked answer

A type assertion performs no runtime validation. I would decode the unknown payload with an explicit validator or schema library, report a structured parse failure, and allow only validated User values into the application domain. Tests include missing, null, wrong-type, and extra-field payloads.

Verify and go deeper