TypeScript: TypeScript Type Guards and Narrowing
Last updated: 2026-08-26
Type narrowing is one of TypeScript’s most practical features—it narrows broad types down to a more precise scope, allowing you to safely access properties and methods of specific types.
1. Overview of Type Narrowing
Variables of a union type can only access properties shared by all members. Type narrowing allows us to "narrow" the type in a specific code branch to access members unique to that branch:
function process(value: string | number) {
// Before narrowing——value Only shared methods can be used
console.log(value.toString()); // ✅ string and number All of them
// After narrowing——value You can use a specific method
if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅ string Unique Approach
} else {
console.log(value.toFixed(2)); // ✅ number Unique Approach
}
}
TypeScript supports the following types of narrowing:
| Narrowing Method | Applicable Scenarios |
|---|---|
typeof |
Basic Type Check |
instanceof |
Class Instance Check |
in Operator |
Does the property exist? |
| Equality Check | ===/!== Comparison |
| Custom Type Guards | Complex Type Checks |
| Narrowable | Automatically narrow during assignment |
2. typeof narrowing
typeof Most commonly used to distinguish between basic types:
(1) Basic Usage
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
return " ".repeat(padding) + value; // padding narrowed to number
}
return padding + value; // padding narrowed to string
}
console.log(padLeft("hello", 4)); // " hello"
console.log(padLeft("hello", ">>>")); // ">>>hello"
(2) The return value of typeof
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 100n // "bigint"
typeof {} // "object" ⚠️ Including null, Array, Date, etc.
typeof function(){} // "function"
typeof null === "object" is a historical bug in JavaScript. You cannot use typeof to distinguish between null and other objects—you need to use === null to check.
(3) typeof with switch
function describe(value: string | number | boolean | undefined) {
switch (typeof value) {
case "string":
return `String:${value.toUpperCase()}`;
case "number":
return `Numbers:${value.toFixed(2)}`;
case "boolean":
return `Boolean:${value}`;
case "undefined":
return "Undefined";
}
}
3. instanceof narrowing
instanceof Check whether an object is an instance of a certain class—suitable for narrowing reference types:
(1) Basic Usage
function formatValue(value: Date | string | Error): string {
if (value instanceof Date) {
return value.toISOString(); // Date Methods
} else if (value instanceof Error) {
return value.message; // Error Properties
} else {
return value.toUpperCase(); // string Methods
}
}
console.log(formatValue(new Date())); // "2024-..."
console.log(formatValue(new Error("Error"))); // "Error"
console.log(formatValue("hello")); // "HELLO"
(2) Limitations of instanceof
instanceof Can only be used with class instances—not with interfaces or type aliases (which do not exist at runtime):
interface Dog { bark(): void; }
interface Cat { meow(): void; }
// ❌ instanceof Cannot be used for interfaces
// if (pet instanceof Dog) { ... }
// ✅ Do you need to use a custom type guard or in Operator
(3) Custom Classes and instanceof
class NetworkError extends Error {
constructor(public statusCode: number) {
super(`Network Error:${statusCode}`);
}
}
class ValidationError extends Error {
constructor(public field: string) {
super(`Validation Error:${field}`);
}
}
function handleError(error: NetworkError | ValidationError): string {
if (error instanceof NetworkError) {
return `HTTP ${error.statusCode} Error`;
} else {
return `Field ${error.field} Invalid`;
}
}
4. Narrowing of the in Operator
in Checks whether an object has a specific property—useful for distinguishing between interfaces and type aliases:
(1) Basic Usage
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // ✅ Fish Type
} else {
animal.fly(); // ✅ Bird Type
}
}
(2) Distinguishable Attributes
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2; // ✅ Circle 's radius
} else {
return shape.sideLength ** 2; // ✅ Square 's sideLength
}
}
▶ Example: Type-Safe Event Handling
Output:
Click here: (100, 200)
Button: Enter, Ctrl: false
Scroll: top=50, left=0
interface ClickEvent {
type: "click";
x: number;
y: number;
}
interface KeyEvent {
type: "keydown" | "keyup";
key: string;
ctrlKey: boolean;
}
interface ScrollEvent {
type: "scroll";
scrollTop: number;
scrollLeft: number;
}
type UIEvent = ClickEvent | KeyEvent | ScrollEvent;
function handleEvent(event: UIEvent): string {
switch (event.type) {
case "click":
return `Click here:(${event.x}, ${event.y})`;
case "keydown":
case "keyup":
return `Button:${event.key},Ctrl:${event.ctrlKey}`;
case "scroll":
return `Scroll:top=${event.scrollTop}, left=${event.scrollLeft}`;
}
}
console.log(handleEvent({ type: "click", x: 100, y: 200 }));
console.log(handleEvent({ type: "keydown", key: "Enter", ctrlKey: false }));
console.log(handleEvent({ type: "scroll", scrollTop: 50, scrollLeft: 0 }));
Output:
Click here:(100, 200)
Button:Enter,Ctrl:false
Scroll:top=50, left=0
5. Custom Type Guards
When the built-in narrowing methods aren't sufficient, you can write custom type guard functions:
(1) Type Predicate
interface Dog {
bark(): void;
breed: string;
}
interface Cat {
meow(): void;
color: string;
}
// Type Predicates:The return value is "Parameter Name is Type"
function isDog(animal: Dog | Cat): animal is Dog {
return "bark" in animal;
}
function interact(animal: Dog | Cat) {
if (isDog(animal)) {
animal.bark(); // ✅ narrowed to Dog
console.log(animal.breed);
} else {
animal.meow(); // ✅ narrowed to Cat
console.log(animal.color);
}
}
(2) Assertion Function
Assertion functions throw an exception when the condition is not met—telling TypeScript, "If this line is executed, the condition must be true":
function assertDefined<T>(value: T | undefined | null, message?: string): asserts value is NonNullable<T> {
if (value == null) {
throw new Error(message ?? "The value cannot be null or undefined");
}
}
function processUser(user: User | undefined) {
assertDefined(user, "User does not exist");
// After that user The type has been narrowed down to User(Excludes undefined)
console.log(user.name.toUpperCase()); // ✅ Safety
}
(3) Assertions vs. Type Predicates
| Property | Type predicate x is T |
Assertion function asserts x is T |
|---|---|---|
| Return Value | boolean | void (throws an exception if not satisfied) |
| Usage | if (isType(x)) |
assertIsType(x) |
| Type Narrowing | Narrowing in an if Branch | Automatic Narrowing After a Call |
| Suitable Scenarios | Post-Check Branch Handling | Prerequisite Checks |
6. Assignment Narrowing
Assignment operations also perform type narrowing:
let value: string | number;
value = "hello";
console.log(value.toUpperCase()); // ✅ After assignment, it narrows to string
value = 42;
console.log(value.toFixed(2)); // ✅ After assignment, it narrows to number
(1) Control Flow Analysis
TypeScript tracks changes in a variable's type throughout the control flow:
function example(x: string | number | boolean) {
// x: string | number | boolean
if (typeof x === "string") {
// x: string
console.log(x.toUpperCase());
} else {
// x: number | boolean
if (typeof x === "number") {
// x: number
console.log(x.toFixed(2));
} else {
// x: boolean
console.log(x);
}
}
}
(2) Narrowing and Reassignment
let value: string | number;
value = "hello";
console.log(value.length); // ✅ string
value = 42;
// console.log(value.length); // ❌ number None length
value = true; // ❌ boolean Not in a composite type
7. Exhaustive Checking
Ensure that the switch/if statement covers all possible types—use the never type to guarantee completeness:
type Shape = "circle" | "square" | "triangle";
function getIcon(shape: Shape): string {
switch (shape) {
case "circle": return "○";
case "square": return "□";
case "triangle": return "△";
default: {
// If all case It's all taken care of.,shape Here is never
const _exhaustive: never = shape;
return _exhaustive;
}
}
}
// If in the future Shape Added "hexagon" But I didn't add it case
// default Branched _exhaustive Report Type Error
// This is a reminder to fill in any missing information. case
(1) A More Concise Exhaustive Check
function assertNever(value: never): never {
throw new Error(`Unprocessed values:${value}`);
}
type Action = "create" | "update" | "delete";
function handleAction(action: Action) {
switch (action) {
case "create": /* ... */ break;
case "update": /* ... */ break;
case "delete": /* ... */ break;
default:
assertNever(action); // If omitted case,A type error will be reported here.
}
}
▶ Example: typeof and instanceof for Type Narrowing
function format(value: string | number | Date): string {
if (typeof value === "string") {
return value.trim().toUpperCase();
} else if (typeof value === "number") {
return value.toFixed(2);
} else if (value instanceof Date) {
return value.toISOString();
}
return String(value);
}
console.log(format(" hello ")); // "HELLO"
console.log(format(3.14159)); // "3.14"
console.log(format(new Date("2024-01-01"))); // "2024-01-01T00:00:00.000Z"
Output:
HELLO
3.14
2024-01-01T00:00:00.000Z
▶ Example: Custom Type Guard with Discriminated Unions
interface Circle { kind: "circle"; radius: number; }
interface Rectangle { kind: "rectangle"; width: number; height: number; }
type Shape = Circle | Rectangle;
function isCircle(shape: Shape): shape is Circle {
return shape.kind === "circle";
}
function area(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2; // Circle
}
return shape.width * shape.height; // Rectangle
}
console.log(area({ kind: "circle", radius: 5 })); // 78.54
console.log(area({ kind: "rectangle", width: 4, height: 6 })); // 24
Output:
78.53981633974483
24
❓ FAQ
typeof and instanceof?typeof checks the "primitive type" of a value (string/number/boolean/undefined/object/function) and returns a string; it is suitable for checking primitive types. instanceof checks whether a value is an instance of a particular class; it is suitable for checking reference types (such as Date, Error, and custom classes). The two are complementary and not mutually exclusive.if statements and property checks. Type predicates (x is T) and assertion functions (asserts x is T) do not exist at runtime, so there is zero overhead.in operator allow type narrowing?"swim" in animal is true, animal must implement an interface that includes swim. This is a logical inference that does not require runtime type information.📖 Summary
- Type narrowing—the process of narrowing a broad type down to a precise range—is key to the safe use of union types.
typeofchecks for a primitive type,instanceofchecks for a class instance, andinchecks whether a property exists- Custom type guards (type predicates
x is T) handle complex types that cannot be distinguished using built-in methods - The assertion function (
asserts x is T) performs a pre-check and throws an exception if the condition is not met. - TypeScript's control flow analysis automatically tracks changes in variable types within branches
- Use the
nevertype for exhaustive checks to ensure thatswitchandifstatements cover all possibilities
📝 Exercises
- Basic Problem (Difficulty ⭐): Write a function
doubleOrRepeat(value: string | number)— ifvalueis a number, multiply it by 2; if it is a string, concatenate it with itself once (e.g., "hi" → "hihi"). Usetypeofto restrict the input. - Advanced Problem (Difficulty ⭐⭐): Define two interfaces,
Admin(with thehasPermissionmethod) andGuest(with therequestAccessmethod), and refine them using the distinguishable attributerole. Write a function that calls different methods based on the role. - Challenge (Difficulty: ⭐⭐⭐): Write a custom type guard
isNonNull<T>(value: T | null | undefined): value is NonNullable<T>, then use it in thefilterNonNull<T>(arr: (T | null | undefined)[]): T[]function to filter out null and undefined values and return a non-empty array.