What does "union narrowing" mean?
What a union type is
First, a reminder: a union type (A | B | C) is a type that can be one of several options.
type Result = string | number;A variable of type Result can be a string or a number:
let value: Result;
value = "hello"; // string
value = 42; // numberBut until TypeScript knows what exactly is inside right now, it will not let you use type-specific properties.
value.toFixed(); // Error - could be a stringWhat narrowing is
Narrowing is the process by which TypeScript reduces the set of possible types, based on checks in the code.
What union narrowing is
Union narrowing is narrowing a type from a union down to one or more specific members of that union, depending on logical conditions, operators, or checks.
Example 1. Narrowing with typeof
function printValue(value: string | number) {
if (typeof value === "string") {
// here value: string
console.log(value.toUpperCase());
} else {
// here value: number
console.log(value.toFixed(2));
}
}TypeScript "narrowed" value from string | number
down to string in one branch and number in the other.
Example 2. Narrowing with an in check
type Cat = { kind: "cat"; meow: () => void };
type Dog = { kind: "dog"; bark: () => void };
function makeSound(animal: Cat | Dog) {
if ("meow" in animal) {
// animal: Cat
animal.meow();
} else {
// animal: Dog
animal.bark();
}
}Example 3. Narrowing through a literal property
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape) {
if (shape.kind === "circle") {
// shape: { kind: "circle"; radius: number }
return Math.PI * shape.radius ** 2;
} else {
// shape: { kind: "square"; side: number }
return shape.side ** 2;
}
}This is an example of discriminated union narrowing
(narrowing by the discriminant, the kind key).
Example 4. Narrowing with instanceof
function logDate(d: Date | string) {
if (d instanceof Date) {
// d: Date
console.log(d.toISOString());
} else {
// d: string
console.log(d.toUpperCase());
}
}Example 5. Narrowing with an == null check
function greet(name?: string | null) {
if (name == null) {
// name: null | undefined
console.log("No name provided");
} else {
// name: string
console.log("Hello, " + name);
}
}Example 6. Narrowing with custom type guards
You can declare your own "type filter" - a function that returns value is ...:
type Car = { wheels: 4; drive: () => void };
type Bike = { wheels: 2; pedal: () => void };
function isCar(v: Car | Bike): v is Car {
return (v as Car).drive !== undefined;
}
function move(vehicle: Car | Bike) {
if (isCar(vehicle)) {
// vehicle: Car
vehicle.drive();
} else {
// vehicle: Bike
vehicle.pedal();
}
}What TypeScript takes into account during narrowing
TypeScript can automatically analyze control flow:
if,else,switchtypeofinstanceofin===,!==,==,!=nullandundefinedchecks- custom guards (
v is T) return/throw/break(affect which branches remain possible)
An example of combined narrowing
function process(val: string | number | boolean | null) {
if (typeof val === "string") {
// val: string
} else if (typeof val === "number") {
// val: number
} else if (val === null) {
// val: null
} else {
// val: boolean (the remaining type)
}
}Why this matters
Without narrowing, TypeScript could not guarantee type safety.
It would not know which type is in use at a given point in the program,
and would force you to write as Type everywhere.
Thanks to union narrowing, TypeScript can analyze the logic of the code and automatically pick the correct subtype.
Summary
| Term | Meaning |
|---|---|
| Union Type | A union of possible types (`A |
| Narrowing | Reducing the set of possible types based on conditions |
| Union Narrowing | Narrowing specifically within a union of types |
| How it happens | Through typeof, instanceof, in, literal checks, guards |
| Main goal | Letting TypeScript know exactly which type is used in each branch of the code |
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.