JavaScript and Type Narrowing Magic

Joel ThomsJoel Thoms
3 min read

Type Narrowing allows the IDE (development environment) to narrow down the object type, from a union type, to a more specific type.

Bullsh*t Example

In this bullsh*t example, value is the union type string | number, meaning it can either be a string or number (ya, please don’t do that).

const value: string | number

The type can be narrowed down by checking the type with an if. Now that the IDE knows the type, it also knows .toUpperCase() only exists on string and .toFixed() only exists on number.

Type Narrowing an API Response

In this example, fetchData can respond with either a Result or ErrorMessage type. We can use an if again to narrow the type down. The IDE now knows inside the if, data is always of type Result and inside the else, it must be of type ErrorMessage.

The IDE now gives us proper Property does not exist error messages when used on the wrong type.

Strict Null Checks

When Strict Null Checks are enabled, we can narrow down data.message type from a Nullable string to string. The error seen on line 17 doesn’t appear on line 20 because the if narrows down the type.

Type Narrowing Magic in Custom Classes

Type Narrowing can also be controlled in custom classes. Inside the if, the IDE knows res is of type Success and in the else it is type Failure.

The type Narrowing is controlled through the JSDocs on the isOk() function like this:

export class Success {
  /** @returns {this is Success<T>} */
  isOk() {
    return true;
  }
}

export class Failure {
  /** @returns {this is Success<any>} */
  isOk() {
    return false;
  }
}

Here’s the full source to the above example:

// @ts-check

/**
 * @template T
 * Represents a successful result.
 */
export class Success {
  /** @param {T} value */
  constructor(value) {
    /** @private */
    this._value = value;
  }

  /** @returns {this is Success<T>} */
  isOk() {
    return true;
  }

  /** @returns {T} */
  value() {
    return this._value;
  }
}

/**
 * @template E
 * Represents a failed result.
 */
export class Failure {
  /** @param {E} error */
  constructor(error) {
    /** @private */
    this._error = error;
  }

  /** @returns {this is Success<any>} */
  isOk() {
    return false;
  }

  /** @returns {E} */
  error() {
    return this._error;
  }
}

/**
 * Result class with static helpers for Success and Failure.
 */
export default class Result {
  /**
   * @template T
   * @param {T} value
   * @returns {Success<T>}
   */
  static success(value) {
    return new Success(value);
  }

  /**
   * @template E
   * @param {E} error
   * @returns {Failure<E>}
   */
  static failure(error) {
    return new Failure(error);
  }
}

const res = Math.random() > 0.5 ? Result.success(42) : Result.failure("error");

if (res.isOk()) {
  console.log(res.value());
  console.log(res.error());
} else {
  console.log(res.value());
  console.log(res.error());
}

Summary

Type Narrowing can be used to narrow down a type from a union type to a specific type. This allows the IDE to know which type you are working with and give errors or warnings about incorrect usage allowing you to write safer code.

Follow me on X

Cheers 🍻

1
Subscribe to my newsletter

Read articles from Joel Thoms directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joel Thoms
Joel Thoms