New: We've launched a brand new Coding Challenges section! Check out these interactive, real-world exercises to level up your skills.Explore Challenges
FrontendPrep
typescriptMedium

TypeScript: Type Narrowing and User-Defined Type Guards

Loading...

Master TypeScript type narrowing. Learn how to use typeof, instanceof, the 'in' operator, and custom type guard functions (parameter value predicates) to write type-safe logic.

Arvind M
Arvind MLinkedIn

TypeScript: Type Narrowing and User-Defined Type Guards

One of the most frequent intermediate to advanced interview questions is:

What is Type Narrowing in TypeScript, and how do you write a custom (User-Defined) Type Guard using the is keyword?

TypeScript analyzes your JavaScript control flow to narrow down types automatically inside conditional checks. When built-in helpers are insufficient, you can create custom predicate functions to direct the type checker.


1. Built-in Type Narrowing Techniques

TypeScript understands standard JavaScript runtime operators:

A. The typeof Operator

Used for checking primitive types (string, number, boolean, symbol, undefined, object, function).

function printLength(value: string | number) {
  if (typeof value === "string") {
    console.log(value.length); // ✅ Type is narrowed to 'string'
  } else {
    console.log(value.toFixed(2)); // ✅ Type is narrowed to 'number'
  }
}

B. The instanceof Operator

Used for checking classes, constructor functions, and prototype structures.

function processDate(value: Date | string) {
  if (value instanceof Date) {
    console.log(value.getTime()); // ✅ Type is narrowed to 'Date'
  }
}

C. The in Operator

Used to check if an object contains a specific property.

interface Admin {
  privileges: string[];
}
interface Employee {
  startDate: Date;
}
 
function printInfo(user: Admin | Employee) {
  if ("privileges" in user) {
    console.log(user.privileges); // ✅ Type is narrowed to 'Admin'
  }
}

2. User-Defined Type Guards (parameter is Type)

Sometimes, object shapes cannot be distinguished easily with built-in runtime operators. To solve this, you can write custom validator functions that return a Type Predicate:

       Normal boolean return
       ┌────────────────────────┐
       │ function isFish(x) {   │ ◄── Returns boolean (Does NOT narrow type)
       │   return x.swim !==    │
       │     undefined;         │
       │ }                      │
       └────────────────────────┘
 
       Type Predicate return
       ┌────────────────────────┐
       │ function isFish(x):    │
       │   x is Fish {          │ ◄── Returns 'x is Fish' (Forces compiler to narrow)
       │   return (x as Fish)   │
       │     .swim !==          │
       │     undefined;         │
       │ }                      │
       └────────────────────────┘

Example Code:

interface Cat {
  meow(): void;
}
interface Dog {
  bark(): void;
}
 
// User-Defined Type Guard
function isCat(pet: Cat | Dog): pet is Cat {
  return (pet as Cat).meow !== undefined;
}
 
function makeNoise(pet: Cat | Dog) {
  if (isCat(pet)) {
    pet.meow(); // ✅ TypeScript knows pet is Cat
  } else {
    pet.bark(); // ✅ TypeScript knows pet must be Dog
  }
}

3. Discriminated Unions

As a cleaner alternative to custom type guards, you can include a common literal status key (discriminant) in your shapes to narrow types automatically:

interface NetworkSuccessState {
  type: "success";
  payload: string;
}
interface NetworkErrorState {
  type: "error";
  code: number;
}
 
type NetworkState = NetworkSuccessState | NetworkErrorState;
 
function handle(state: NetworkState) {
  switch (state.type) {
    case "success":
      console.log(state.payload); // Narrowed to NetworkSuccessState
      break;
    case "error":
      console.log(state.code);    // Narrowed to NetworkErrorState
      break;
  }
}

Key Takeaways

  • Type Predicates: Return parameterName is Type from a function to tell the compiler that returning true guarantees the variable type.
  • Narrowing Operators: Leverage typeof, instanceof, and in for standard Javascript object structures.
  • discriminated unions: Use literal fields to create robust control flows that scale without writing manual checks.
  • exhaustive checking: Use the never type in your switch fallbacks to guarantee all union states are handled.

Finished practicing this challenge?

Mark it as completed to track your progress, or bookmark it to review later.

Loading...

Share this Resource

Help other developers level up by sharing this study guide.

⚡ Weekly newsletter

Crack Your Next Frontend Interview.

Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.

  • Senior level React, JS, and CSS interview blueprints
  • System Design & performance optimization deep-dives
  • 100% free, zero spam, unsubscribe with one click

Join the Study Track

We value your privacy. Unsubscribe at any time.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.