Type Narrowing
Learn how TypeScript narrows a broad or union type down to a more specific one within conditional checks.
Reading Time
18 min
Lesson
Lesson 21 of 30
What is Narrowing?
Narrowing is how TypeScript deduces a more specific type for a value within a certain block of code, based on a runtime check you’ve already written — like an if statement.
typeof Narrowing
function format(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // narrowed to string
}
return value.toFixed(2); // narrowed to number
}Truthiness Narrowing
A simple truthy check can narrow out null, undefined, "", 0, and NaN, which is especially useful for optional values.
Truthiness Narrowing Example
function printLength(value?: string) {
if (value) {
console.log(value.length); // narrowed: value is 'string', not 'string | undefined'
}
}instanceof Narrowing
instanceof narrows a value based on which class it was constructed from — commonly used with union types made of different class instances.
instanceof Narrowing Example
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
function handle(error: Error | ApiError) {
if (error instanceof ApiError) {
console.log(error.statusCode); // narrowed to ApiError
}
}in Narrowing
The in operator narrows based on whether an object has a particular property, useful for unions of plain object shapes that don’t share a class hierarchy.
in Narrowing Example
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function makeSound(animal: Cat | Dog) {
if ("meow" in animal) {
animal.meow(); // narrowed to Cat
} else {
animal.bark(); // narrowed to Dog
}
}Discriminated Unions
When every member of a union shares a common literal property (a "tag"), checking that property narrows the entire object automatically — this is the most robust and scalable narrowing pattern.
Discriminated Union Narrowing
type Result =
| { status: "success"; data: string }
| { status: "error"; message: string };
function handleResult(result: Result) {
if (result.status === "success") {
console.log(result.data); // narrowed: 'data' exists on this branch
} else {
console.log(result.message); // narrowed: 'message' exists on this branch
}
}Custom Type Guards Extend Narrowing
When built-in checks aren’t enough, you can write your own type predicate function (covered in a later lesson) to teach TypeScript how to narrow a type based on your own custom logic.
Best Practice
Prefer discriminated unions over loosely related optional properties whenever a value can take a few distinct shapes — the narrowing is exhaustive, safe, and reads clearly at every call site.