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

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
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.
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.
function format(x: string | number) { if (typeof x === 'string') { return x.trim(); // x is string here } return x.toFixed(2); // x is number here}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.
function keys(x: object | null) { if (typeof x === 'object' && x !== null) { return Object.keys(x); // without the null check, x could be null } return [];}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.
function isUser(v: unknown): v is User { return typeof v === 'object' && v !== null && 'id' in v;}
if (isUser(payload)) { payload.id; // narrowed to User}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.
const a: any = JSON.parse(s);a.foo.bar; // no error — unsafe
const u: unknown = JSON.parse(s);u.foo; // Error — must narrow firstAdvanced · 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.
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}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.
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; } }}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.
- Treat decoded JSON as unknown.
- Validate required fields and nested shapes at the boundary.
- Return a typed success/error result.
- 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.