What is type narrowing in TypeScript?
What is Type Narrowing
Type narrowing is the process by which TypeScript refines a variable's type based on logical checks, operators, and the flow of the code.
In simpler terms: TypeScript "watches" your
if,typeof,in,instanceof, and other checks, and figures out which type is actually possible in each branch of the code.
Example: without narrowing
function printId(id: string | number) {
console.log(id.toUpperCase()); // Error - id can be a number
}id has the type string | number,
and TypeScript does not know which one is currently in use.
Example with narrowing
function printId(id: string | number) {
if (typeof id === "string") {
// Here id: string
console.log(id.toUpperCase());
} else {
// Here id: number
console.log(id.toFixed(2));
}
}TypeScript has narrowed the type of id:
string in the first branch, number in the second.
How narrowing works
TypeScript performs control flow analysis and automatically "cuts off" impossible types when it encounters:
- checks with
typeof,instanceof,in,===,!==,==,!=; - checks against
null/undefined; - custom type guards (
value is Type); - logical statements (
if,else,switch,return,throw, etc.).
The main ways to narrow types
| Method | Description | Example |
|---|---|---|
typeof | For primitives | typeof x === "string" |
instanceof | For classes and constructors | x instanceof Date |
in | Checks whether a property exists | "prop" in obj |
=== / !== | Comparison of literal values | x === null |
null / undefined checks | Excludes "empty" values | if (x) |
| Custom type guard | A function of the form x is T | isArray(value): value is any[] |
Examples of different narrowing variants
1. typeof - primitives
function log(value: string | number | boolean) {
if (typeof value === "string") {
value.toUpperCase(); // string
} else if (typeof value === "number") {
value.toFixed(2); // number
} else {
value.valueOf(); // boolean
}
}2. instanceof - classes and objects
function logDate(d: Date | string) {
if (d instanceof Date) {
d.toISOString(); // Date
} else {
d.toUpperCase(); // string
}
}3. in - checking whether a property exists
type Dog = { bark: () => void };
type Cat = { meow: () => void };
function speak(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark(); // Dog
} else {
animal.meow(); // Cat
}
}4. == null checks
function greet(name?: string | null) {
if (name == null) {
console.log("No name provided");
} else {
console.log("Hello, " + name.toUpperCase());
}
}After if (name == null) TypeScript narrows the type to null | undefined,
and after else, to string.
5. With a custom type guard
function isString(value: unknown): value is string {
return typeof value === "string";
}
function print(value: unknown) {
if (isString(value)) {
value.toUpperCase(); // now definitely a string
}
}Custom type guards are a way to teach TS to "recognize" your own types.
6. With literal types and comparisons
type Status = "loading" | "success" | "error";
function handle(status: Status) {
if (status === "loading") {
// status: "loading"
} else if (status === "success") {
// status: "success"
} else {
// status: "error"
}
}This is a special case of discriminated union narrowing (narrowing by a discriminant property).
Narrowing in union types (Union Narrowing)
If a variable has the type A | B | C,
TypeScript will narrow it to a specific subtype
once a condition rules out the rest.
function example(x: string | number | boolean) {
if (typeof x === "string") {
// string
} else if (typeof x === "number") {
// number
} else {
// boolean
}
}Narrowing to never
When TypeScript rules out every possible option,
the variable gets the type never (an unreachable type).
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value.toFixed(2);
} else {
// value: never (this branch cannot be reached)
throw new Error("Unexpected type");
}
}Why this matters
Safety: TS will not let you call a method that does not belong to the type.
Convenience: The IDE suggests the available properties and methods after narrowing.
Clear code:
You can write clean if/switch statements without unnecessary casts (as).
The basis of discriminated unions: Without narrowing, TS would not be able to tell which variant of a union type is in use.
Summary
| Concept | Description |
|---|---|
| Type narrowing | The process of refining a variable's type based on conditions |
| Why it is needed | So TypeScript knows which subtype is actually in use at a given point |
| When it triggers | On checks (typeof, instanceof, in, ===, is, null, etc.) |
| Result | TS "narrows" a union or unknown type down to a specific one |
| The "never" type | Used when no other option remains |
Just remember:
Type narrowing is when TypeScript "thinks like a human": it reads your
ifand figures out what type the value has inside each branch.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.