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).

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

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

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

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

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