FrontendPrep
Menu
Topics
Questions
Guides
Challenges
Soon
Back to TypeScript Questions
typescriptMedium

TypeScript: Type Narrowing and User-Defined Type Guards

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.

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.

Share this Resource

Help other developers level up by sharing this study guide.

More Technical Questions

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