JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeTopicstypescriptType Guards & Narrowing
PrevNext
typescript
intermediate
9 min read

Type Guards & Narrowing

assertion-functions
control-flow
discriminated-unions
instanceof
narrowing
type-guards
type-predicates
typeof

Type narrowing is TypeScript's ability to refine broad types into specific ones within conditional blocks — typeof checks primitives, instanceof checks classes, in checks properties, and custom type predicates (param is Type) enable reusable narrowing logic for discriminated unions and complex shapes.

Key Points

1Built-in Guards

typeof narrows primitives, instanceof narrows class instances, in checks property existence — all trigger automatic type narrowing in conditional blocks.

2Discriminated Unions

A common literal property (status, kind, type) enables exhaustive narrowing in if/switch — the most type-safe pattern for union types.

3Custom Type Predicates

Functions returning param is Type encapsulate reusable narrowing logic — TypeScript trusts the predicate, so correctness is the developer's responsibility.

4Control Flow Analysis

TypeScript tracks types through if/else, switch, return, throw, and logical operators — assignments and truthiness checks also narrow types automatically.

What You'll Learn

  • Use typeof, instanceof, and in operators for type narrowing
  • Implement discriminated unions with exhaustive checking
  • Write custom type predicates and assertion functions for reusable narrowing
  • Understand how TypeScript's control flow analysis tracks types through branches

Deep Dive

Type narrowing is how TypeScript refines a broad type into a more specific one based on runtime checks. When you check typeof x === 'string', TypeScript narrows x from string | number to string inside that branch. This is fundamental to working with union types safely.

typeof Guards

The typeof operator narrows primitive types:

TypeScript
function format(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase(); // value: string
  }
  return value.toFixed(2); // value: number
}

typeof works for string, number, boolean, symbol, bigint, undefined, function, and object. Note: typeof null === 'object' — TypeScript knows this quirk and handles it correctly when you check for null separately.

instanceof Guards

instanceof narrows to class instances:

TypeScript
function handleError(error: Error | string) {
  if (error instanceof TypeError) {
    console.log(error.message); // error: TypeError
  } else if (typeof error === 'string') {
    console.log(error); // error: string
  }
}

instanceof checks the prototype chain — it works with classes and constructor functions, not plain interfaces or type aliases.

in Operator

The in operator checks if a property exists on an object:

TypeScript
type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim(); // animal: Fish
  } else {
    animal.fly(); // animal: Bird
  }
}

in narrows based on property presence — useful when types don't share a common discriminant field.

Discriminated Unions

The most powerful narrowing pattern uses a common literal property (the discriminant):

TypeScript
type Success = { status: 'success'; data: string };
type Failure = { status: 'error'; message: string };
type Result = Success | Failure;
 
function handle(result: Result) {
  if (result.status === 'success') {
    console.log(result.data); // result: Success
  } else {
    console.log(result.message); // result: Failure
  }
}

TypeScript sees the status literal check and narrows to the matching union member. This works in if, switch, and ternary expressions. Exhaustive checking in switch uses never in the default case to ensure all variants are handled.

Custom Type Predicates

For complex narrowing logic, write a function with a type predicate return type:

TypeScript
function isString(value: unknown): value is string {
  return typeof value === 'string';
}
 
function isFish(animal: Fish | Bird): animal is Fish {
  return 'swim' in animal;
}

The param is Type return type tells TypeScript that if the function returns true, the parameter is the specified type. This lets you encapsulate narrowing logic in reusable functions.

Type predicates require manual correctness — TypeScript trusts that your runtime check matches the declared type predicate. A wrong predicate (returning true when the value isn't actually that type) causes unsound behavior.

Assertion Functions

Assertion functions narrow by throwing on failure rather than returning a boolean:

TypeScript
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw new TypeError('Expected string');
}
 
let x: unknown = getValue();
assertIsString(x);
x.toUpperCase(); // x: string (narrowed after assertion)

The asserts param is Type signature tells TypeScript that after the function returns (without throwing), the parameter has been narrowed.

Control Flow Analysis

TypeScript's narrowing works through control flow analysis — it tracks type information through if/else, switch, return, throw, and &&/||/?? operators. Assignments also narrow: let x: string | null = getString(); if (x === null) return; // x: string after this point.

Key Interview Distinction

Type narrowing refines broad types into specific ones through runtime checks. typeof for primitives, instanceof for classes, in for property existence, equality checks for discriminated unions. Custom type predicates (param is Type) encapsulate narrowing logic. Assertion functions (asserts param is Type) narrow by throwing on failure. TypeScript tracks narrowing through control flow analysis — it follows your conditional logic to determine the type at each point.

Fun Fact

TypeScript's control flow analysis was a breakthrough when it landed in TypeScript 2.0. Before that, you had to use explicit type assertions (<string>value or value as string) everywhere — the compiler couldn't track that an if check had narrowed a union type. Anders Hejlsberg demonstrated it at the TypeScript 2.0 launch and the audience reaction was so enthusiastic that the video clip went viral in the developer community.

Learn These First

Union & Intersection Types

beginner

Basic Types

beginner

Continue Learning

Function Types

beginner

Interfaces vs Type Aliases

beginner

Practice What You Learned

What are type guards in TypeScript and how do you create custom ones?
mid
type-guards
Type guards are runtime checks that narrow types within conditional blocks. Built-in guards include typeof, instanceof, and in operators. Custom type guards are functions returning a type predicate (param is Type) to narrow types based on custom logic.
Previous
Template Literal Types
Next
Basic Types
PrevNext