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:

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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"
🔥 Common Mistake: 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

TYPESCRIPT
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

TYPESCRIPT
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):

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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:

TEXT 📖 Display only
Click here: (100, 200)
Button: Enter, Ctrl: false
Scroll: top=50, left=0
TYPESCRIPT
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:

TEXT 📖 Display only
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

TYPESCRIPT
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":

TYPESCRIPT
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:

TYPESCRIPT
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:

TYPESCRIPT
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

TYPESCRIPT
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:

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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"
▶ Try it Yourself

Output:

TEXT 📖 Display only
HELLO
3.14
2024-01-01T00:00:00.000Z

▶ Example: Custom Type Guard with Discriminated Unions

TYPESCRIPT
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
▶ Try it Yourself

Output:

TEXT 📖 Display only
78.53981633974483
24

❓ FAQ

Q What is the difference between typeof and instanceof?
A 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.
Q What is the performance impact of custom type guards?
A None. Type guards are used only at compile time—the compiled JavaScript consists of ordinary 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.
Q Why does the in operator allow type narrowing?
A Because TypeScript knows that if an object has a certain property, it must belong to an interface that includes that property. When "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.
Q When do you need to write custom type guards?
A When built-in type narrowing methods (typeof, instanceof, in, ===) cannot distinguish between types. The most common scenario is distinguishing between interfaces—since interfaces do not exist at runtime, they cannot be checked using instanceof; instead, you must use in to check for identifiable properties or write custom type guards.

📖 Summary

📝 Exercises

  1. Basic Problem (Difficulty ⭐): Write a function doubleOrRepeat(value: string | number) — if value is a number, multiply it by 2; if it is a string, concatenate it with itself once (e.g., "hi" → "hihi"). Use typeof to restrict the input.
  2. Advanced Problem (Difficulty ⭐⭐): Define two interfaces, Admin (with the hasPermission method) and Guest (with the requestAccess method), and refine them using the distinguishable attribute role. Write a function that calls different methods based on the role.
  3. Challenge (Difficulty: ⭐⭐⭐): Write a custom type guard isNonNull<T>(value: T | null | undefined): value is NonNullable<T>, then use it in the filterNonNull<T>(arr: (T | null | undefined)[]): T[] function to filter out null and undefined values and return a non-empty array.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏