What is type narrowing?
What is Type Narrowing
Type narrowing is a process in which TypeScript understands exactly what type a value has in a particular branch of code and narrows the general type down to a more specific one.
Put simply: If a variable can have several types (for example,
string | number), TypeScript "narrows" that set when you perform checks such astypeof,in,instanceof, and so on.
Example 1. Narrowing via typeof
function printId(id: string | number) {
if (typeof id === "string") {
// Here id: string
console.log("String:", id.toUpperCase());
} else {
// Here id: number
console.log("Number:", id.toFixed(2));
}
}TypeScript understands on its own:
- inside
if (typeof id === "string")->idbecamestring - inside
else->idbecamenumber
Example 2. Narrowing via instanceof
class User {
constructor(public name: string) {}
}
class Admin {
constructor(public name: string, public permissions: string[]) {}
}
function printPerson(person: User | Admin) {
if (person instanceof Admin) {
// Here person: Admin
console.log("Permissions:", person.permissions);
} else {
// Here person: User
console.log("Regular user:", person.name);
}
}Example 3. Narrowing via in (property presence)
type User = { name: string };
type Admin = { name: string; permissions: string[] };
function printInfo(person: User | Admin) {
if ("permissions" in person) {
// person: Admin
console.log("Permissions:", person.permissions);
} else {
// person: User
console.log("Name:", person.name);
}
}
inchecks for the presence of a property, and TypeScript uses this for logical type narrowing.
Example 4. Narrowing via ==, ===, !==, !=
TypeScript also knows how to "understand" the type after such checks:
function process(value: string | null) {
if (value !== null) {
// value: string
console.log(value.toUpperCase());
}
}Example 5. Narrowing via "truthy/falsy" values
TypeScript knows that null, undefined, 0, "", false are "falsy" values.
function printName(name?: string) {
if (name) {
// name: string
console.log("Hello,", name.toUpperCase());
} else {
// name: undefined
console.log("Name not provided");
}
}Example 6. Custom type checks (Type Guards)
You can create your own function that helps TypeScript narrow a type:
type User = { name: string };
type Admin = { name: string; permissions: string[] };
function isAdmin(person: User | Admin): person is Admin {
return "permissions" in person;
}
function handle(person: User | Admin) {
if (isAdmin(person)) {
// Here TS knows that person: Admin
console.log("Permissions:", person.permissions);
} else {
// Here person: User
console.log("User:", person.name);
}
}
person is Adminis a user-defined type guard, which tells TS exactly what type is returned from the check.
Example 7. Narrowing inside a switch
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
}
}Here TypeScript uses a discriminated union and automatically narrows the type in each
casebranch.
Where type narrowing is used
| Scenario | Example check |
|---|---|
| Primitives | typeof value === "string" |
| Classes | instanceof SomeClass |
| Objects | "prop" in obj |
| Comparisons | if (x !== null) |
| Boolean checks | if (value) |
| Custom guard functions | value is Type |
| switch discrimination | case "circle": … |
Why it's needed
Type narrowing lets you:
- work without errors with union types;
- get autocomplete and hints in the IDE;
- write safe and readable code without unnecessary type casts (
as).
In short:
Type narrowing is when TypeScript "understands" exactly what type a variable has at a given moment, and automatically narrows the general type down to a specific one across different code branches.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.