TypeScript Type Narrowing
Type narrowing is the process of reducing a broad type to a more specific one based on conditions in the code. When a variable can be a string or a number, TypeScript does not know which one to work with until the code checks it. Inside that check, TypeScript automatically "narrows" the type to the one that matches — allowing safe access to type-specific methods and properties.
Why Narrowing Exists
BEFORE NARROWING:
value: string | number
└── TypeScript: "It could be either — I won't let you use string methods yet"
AFTER NARROWING (inside an if-block):
if (typeof value === "string") {
value ← TypeScript: "Now I know it's a string"
value.toUpperCase() ← allowed
}
typeof Narrowing
The typeof operator is the most common narrowing tool. TypeScript recognizes typeof checks and narrows the type inside the matching block.
function formatValue(value: string | number | boolean): string {
if (typeof value === "string") {
return value.toUpperCase(); // string methods available
}
if (typeof value === "number") {
return value.toFixed(2); // number methods available
}
if (typeof value === "boolean") {
return value ? "YES" : "NO"; // boolean usage
}
// TypeScript knows no other type can reach here
return "";
}
console.log(formatValue("hello")); // HELLO
console.log(formatValue(3.14159)); // 3.14
console.log(formatValue(true)); // YES
Equality Narrowing
Comparing a value with === or !== also narrows the type.
function handleStatus(status: "success" | "error" | "pending"): void {
if (status === "success") {
console.log("Operation completed!");
} else if (status === "error") {
console.log("Something went wrong.");
} else {
// TypeScript knows: only "pending" remains here
console.log("Still processing...");
}
}
in Operator Narrowing
The in operator checks if a property exists on an object. TypeScript narrows the type to the object type that has that property.
interface Car {
make: string;
numberOfWheels: number;
}
interface Bicycle {
brand: string;
gearCount: number;
}
function describeVehicle(vehicle: Car | Bicycle): void {
if ("make" in vehicle) {
// TypeScript: vehicle is Car here
console.log(`Car: ${vehicle.make} with ${vehicle.numberOfWheels} wheels`);
} else {
// TypeScript: vehicle is Bicycle here
console.log(`Bicycle: ${vehicle.brand} with ${vehicle.gearCount} gears`);
}
}
describeVehicle({ make: "Toyota", numberOfWheels: 4 });
// Car: Toyota with 4 wheels
describeVehicle({ brand: "Trek", gearCount: 21 });
// Bicycle: Trek with 21 gears
instanceof Narrowing
The instanceof operator checks if an object was created from a specific class. TypeScript narrows to that class inside the block.
class Teacher {
constructor(public name: string, public subject: string) {}
getRole(): string {
return `${this.name} teaches ${this.subject}`;
}
}
class Principal {
constructor(public name: string, public schoolName: string) {}
getRole(): string {
return `${this.name} heads ${this.schoolName}`;
}
}
function introduce(person: Teacher | Principal): void {
if (person instanceof Teacher) {
// TypeScript: person is Teacher here
console.log(person.getRole());
console.log(`Subject: ${person.subject}`);
} else {
// TypeScript: person is Principal here
console.log(person.getRole());
console.log(`School: ${person.schoolName}`);
}
}
introduce(new Teacher("Ms. Rao", "Mathematics"));
// Ms. Rao teaches Mathematics
// Subject: Mathematics
introduce(new Principal("Mr. Kapoor", "GovtSchool"));
// Mr. Kapoor heads GovtSchool
// School: GovtSchool
Narrowing Flow Diagram
value: string | number | null
│
┌──────┴──────────────────────────────┐
│ │
typeof === "string" typeof === "number"
│ │
string number
(use .toUpperCase) (use .toFixed, arithmetic)
│
null check
value !== null
│
handled
User-Defined Type Guards
A type guard function returns a boolean but with a special return type param is Type. TypeScript uses this to narrow the type inside an if block that calls the function.
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
// Type guard function
function isDog(animal: Cat | Dog): animal is Dog {
return (animal as Dog).bark !== undefined;
}
function makeNoise(animal: Cat | Dog): void {
if (isDog(animal)) {
animal.bark(); // TypeScript knows it's a Dog here
} else {
animal.meow(); // TypeScript knows it's a Cat here
}
}
let pet: Dog = { bark: () => console.log("Woof!") };
makeNoise(pet); // Woof!
Discriminated Unions
A discriminated union is a union of types that each have a common literal property (called the discriminant). TypeScript uses that property to narrow automatically — no extra checks needed.
type Circle = { shape: "circle"; radius: number };
type Rectangle = { shape: "rectangle"; width: number; height: number };
type Triangle = { shape: "triangle"; base: number; height: number };
type Shape = Circle | Rectangle | Triangle;
function getArea(s: Shape): number {
switch (s.shape) { // "shape" is the discriminant
case "circle":
return Math.PI * s.radius * s.radius; // s is Circle
case "rectangle":
return s.width * s.height; // s is Rectangle
case "triangle":
return 0.5 * s.base * s.height; // s is Triangle
}
}
console.log(getArea({ shape: "circle", radius: 7 })); // 153.94
console.log(getArea({ shape: "rectangle", width: 8, height: 5 })); // 40
console.log(getArea({ shape: "triangle", base: 6, height: 4 })); // 12
Narrowing Methods Summary
| Method | Syntax | Best For |
|---|---|---|
typeof | typeof x === "string" | Primitives: string, number, boolean |
instanceof | x instanceof ClassName | Class instances |
in | "property" in obj | Interfaces / objects with unique properties |
| Equality | x === "value" | Literal types and union of literals |
| Null check | x !== null | Nullable types |
| Type guard function | x is Type | Complex custom checks |
| Discriminated union | Common literal property | Union of related object types |
