Lesson 8 of 25
Narrowing
Turn a union into one specific type inside a branch using typeof, instanceof, in, truthiness, and discriminated unions — the core skill of working with TypeScript day to day.
A union type describes what a value might be. Narrowing is how you find out, inside a specific branch of code, exactly which one it is — and it's the single most-used skill in everyday TypeScript, because almost nothing useful happens with a union until you've narrowed it.
typeof narrowing
For primitives, a typeof check does double duty — it's the same runtime
check JavaScript always had, and TypeScript reads it to narrow the type:
function describe(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // value is string here
}
return value.toFixed(2); // value is number here — the only option left
}No annotation, no cast, nothing TypeScript-specific to learn beyond the
typeof check you'd already write in plain JavaScript. TypeScript's control
flow analysis follows the branches and narrows automatically.
instanceof narrowing
For classes, instanceof narrows the same way typeof does for
primitives — genuinely important for handling errors, since a caught value
in TypeScript is typed unknown (covered in depth soon), and JavaScript
permits throwing any value at all, not just Error instances:
try {
riskyOperation();
} catch (error) {
if (error instanceof Error) {
console.log(error.message); // safe — Error definitely has .message
} else {
console.log("Something was thrown that wasn't an Error:", error);
}
}in narrowing
The in operator checks whether a property exists on an object at runtime,
and TypeScript narrows a union down to whichever members actually declare
that property:
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
function area(shape: Circle | Square) {
if ("radius" in shape) {
return Math.PI * shape.radius ** 2; // shape is Circle here
}
return shape.side ** 2; // shape is Square here
}Discriminated unions
The pattern above works, but there's a more direct and far more common version: give every member of the union one shared property holding a distinct literal value (a "discriminant" or "tag"), and switch on that directly:
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;
function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // shape is Circle here
case "square":
return shape.side ** 2; // shape is Square here
}
}TypeScript specifically recognises this shape — one property, one literal
type per union member — and narrows on every comparison against it, whether
that's a switch, an if, or ===. This is the pattern behind type: "loading" | "success" | "error" state objects you'll see constantly in real
applications, and it's worth reaching for deliberately rather than
discovering it by accident.
Truthiness narrowing
A plain if (value) narrows out falsy values (undefined, null, "",
0, NaN, false) the same way it does at runtime:
function greet(name: string | undefined) {
if (!name) {
return "Hello, stranger";
}
return `Hello, ${name}`; // name is string here
}Be careful with this one on numbers: if (count) also excludes 0, which
is usually not what you mean if 0 is a legitimate, meaningful value rather
than an absence.
Exhaustiveness checking with never
A real benefit of discriminated unions: if you later add a new shape to the
union and forget to handle it somewhere, TypeScript can catch that at
compile time, using the fact that never accepts no values at all:
type Shape = Circle | Square; // imagine a Triangle is added here later
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
default: {
const exhaustive: never = shape; // errors if a case was missed
throw new Error(`Unhandled shape: ${exhaustive}`);
}
}
}If a Triangle member is added to Shape later and nobody updates this
function, shape inside default is no longer narrowed down to nothing
(never) — it's still Triangle — and assigning it to a variable typed
never becomes a compile error, right where the new case needs handling.
Try it yourself
What to remember
typeof,instanceof, andinall narrow a union down to a subset of its members, following the same runtime checks JavaScript already has.- A discriminated union (a shared literal "tag" property across every member) is the most common and most reliable pattern for modelling "one of several shapes."
- Truthiness narrowing excludes falsy values, but treat
if (count)carefully when0is a meaningful value. - Assigning to a variable typed
neverin adefaultbranch catches missing cases at compile time as a union grows.
Check yourself
4 questions · pass 3/4 to unlock Arrays, Tuples, and Enums
1.Inside
if (typeof value === "string") { ... }, what is the type of value within that block, givenvalue: string | number?2.Which check correctly narrows
error: unknownbefore readingerror.message?3.For a discriminated union like
{ kind: "circle"; radius: number } | { kind: "square"; side: number }, what narrows it correctly?4.What does the
inoperator check narrow on, e.g.if ("radius" in shape)?
4 left to answer