Logo Blog

Narrowing

The process of turning a general type into a more specific type is called narrowing (verengen / kleiner machen).

Consider this type declaration:

type StringOrArray = string | string[];

const input: StringOrArray =
  Math.random() > 0.5 ? "string" : ["string", "array"];

We cannot really work with this type unless we determine the actual runtime type of our variable.

To determine the type of a variable at runtime, we can use if statements and other type assertions.

if (typeof input == "string") {
  // We can now use string methods
  console.log(input.toUpperCase());
}

The if statement ensures that the type of input is string and TypeScript, therefore, narrows the type of input from string | string[] to string.

TypeScript is also able to determine what the type is going to be in the else branch in this example:

if (typeof input == "string") {
  console.log(input.toUpperCase());
} else {
  // input is of type string[]
  console.log(input.map((str) => str.toLowerCase()));
}

Because input can either be of type string or string[], TypeScript can conclude that if input is not a string, then it has to be of type string[].

Any and Unknown

Narrowing also works on types like any and unknown.

function func(input: unknown) {
  if (typeof input === "string") {
    console.log("input is of type string");
  }
}

Type guards

There are many different ways to perform these runtime checks (also called type guards).

Primitive types

function func(input: boolean | number | string) {
  if (typeof input === "boolean") { ... }
  if (typeof input === "number") { ... }
  if (typeof input === "string") { ... }
}

Arrays

function func(input: string | string[]) {
  if (Array.isArray(input)) {
    console.log("input is a string[]");
  }
}

Classes (instanceof)

function func(input: Date | string) {
  if (input instanceof Date) {
    input.getMonth();
  }
}

By property (in operator)

type City = { zipCode: string };
type Country = { isoCode: string };

function func(input: City | Country) {
  if ("zipCode" in input) {
    return input.zipCode;
  }

  return input.isoCode;
}

Discriminated uniton (tagged union)

interface PushEvent {
  type: "Push";
  repository: string;
}

interface LoginEvent {
  type: "Login";
  email: string;
  timestamp: string;
}

type Event = PushEvent | LoginEvent;

function processEvent(event: Event) {
  switch (event.type) {
    case "Login":
      // event narrowed to: LoginEvent based on type="Login"
      return event.email;
    case "Push":
      // event narrowed to: PushEvent based on type="Push"
      return event.repository;
  }
}

Custom type predicates (is)

TODO: