logo image Faisal Rahman
ID EN
My profile picture taken in the summer Faisal Rahman

Type Narrowing Techniques in TypeScript


TypeScript has operators for use with data types, one of which is the union operator (|). The union operator is used to combine multiple data types, for example:

function getUser(id: number | string) {
  // ...
}

getUser(42); // no TypeError
getUser("42"); // also no TypeError

Simply put, the union operator can be likened to the OR operator; const a: number | string; can be read as “variable a can be of type number or string.” A data type built with the union operator is called a union type.

Union types provide flexibility in attaching data types. However, when working with union types we may simultaneously need certainty about the data type. Consider the code example below:

type Novel = {
  author: string;
  synopsis: string;
};

type TextBook = {
  author: string;
  subject: string;
};

Say we want to create a function to get a summary of a book’s data, containing the author’s name and synopsis if available.

type Summary = { author: string; synopsis?: string };

function getSummary(book: Novel | TextBook): Summary {
  return {
    author: book.author,
    synopsis: book.synopsis, // error
  };
}

You might find this strange. Why does an error occur at book.synopsis but not at book.author? To understand this, we need to understand TypeScript’s Structural Typing system.

Structural Typing

The type system adopted by TypeScript is Structural Typing. In Structural Typing, a data type is considered equivalent to another data type if it has the same property structure, or is a superset of the property structure of the type it is being compared against.

type Square = {
  length: number;
};

type Line = {
  length: number;
};

type Rectangle = {
  length: number;
  width: number;
};

const l: Line = { length: 5 };
const s: Square = l; // Line can be assigned to Square because they have the same structure

const r: Rectangle = { length: 2; width: 3 };
const q: Square = r; // Rectangle can be assigned to Square because Rectangle's structure is a superset of Square

In contrast, in programming languages with a Nominal Typing type system, equivalence and compatibility between types are determined by their explicit declarations. This can be seen in the following Java code snippet:

class Foo {
  public String name;
}

class Bar {
  public String name;
}

Foo n = new Bar(); //error: incompatible types: Bar cannot be converted to Foo

Going back to our first example, why is accessing the author property not an error in the Novel | TextBook type, while synopsis is?

What TypeScript knows is that book can be of type Novel or TextBook. But TypeScript does not know the exact type of book at the point where the synopsis property is accessed. It may or may not have a synopsis property.

However, TypeScript knows that both Novel and TextBook have an author property of type string. So, accessing the author property is always safe, regardless of the actual type of book at the time that property is accessed.

In this example, we want the getSummary function to be able to operate with a book parameter of any type. But at the same time, we need to access the synopsis property only when book can be confirmed to be a Novel. We can rely on TypeScript’s Type Narrowing feature to achieve this.

Type Narrowing and Type Guards

Behind the scenes, TypeScript can perform the process of narrowing a data type to something more specific than the declared type. This process is called Type Narrowing. TypeScript analyzes the code we write and looks for the presence of special patterns that help it infer the most specific data type within a given code block. These special patterns are called Type Guards.

There are many type guard patterns we can use to solve the dilemma in our code example above. Let’s examine some of the most commonly used patterns: the in operator, discriminated unions, custom type guard functions with type predicates, and the typeof operator.

The in Operator

First, let’s try using the in type guard. in is a JavaScript operator that checks for the existence of a property in an object instance. It returns true if the property is found.

If we apply it to our getSummary function, here is what it looks like:

function getSummary(book: Novel | TextBook): Summary {
  if ("synopsis" in book) {
    return { author: book.author, synopsis: book.synopsis };
  }

  return { author: book.author };
}

In the if ('synopsis' in book) { // ... } block, TypeScript analyzes that among the Novel | TextBook data types, only Novel has a synopsis property. So TypeScript can confidently infer that book is of type Novel within that block.

TypeScript also detects that there is a return statement in that block. TypeScript understands that in the subsequent block, book does not have synopsis, so it can be inferred that book is a TextBook.

Discriminated Union

A discriminated union is a union type that has a discriminant that can be used for type narrowing. A discriminant is a non-optional property present in all members of the union type, which has different literal data types across its members.

A literal data type (literal type) is an exact value used as a data type, for example type Apple = 'apple'. Values of string, number, or boolean data types can be used as literal types.

For our use case, we can add a discriminant to each type:

type Novel = {
  kind: "NOVEL";
  author: string;
  synopsis: string;
};

type TextBook = {
  kind: "TEXT_BOOK";
  author: string;
  subject: string;
};

type Summary = { author: string; synopsis?: string };

function getSummary(book: Novel | TextBook): Summary {
  if (book.kind === "NOVEL") {
    return { author: book.author, synopsis: book.synopsis };
  }

  return { author: book.author };
}

This pattern makes the data types more explicit, but the downside is that we have to maintain the discriminant on every member of the union.

The typeof Operator

Another approach is to use another JavaScript operator: typeof. The typeof operator as a type guard can be considered when there is a property with the same name but different data types across all members of the union type.

The typeof operator returns the name of a variable’s data type. Since the check is performed against the data type, TypeScript can use this operator for type narrowing.

Say we have a printAuthor function that prints the name or names of a book’s author.

function printAuthor(author: string | string[]): string {
  if (typeof author === "string") {
    return `The author is ${author}.`;
  } else {
    return `The authors are ${author.join(",")}`;
  }
}

In the else { // ... } block, TypeScript knows that the data type of author is string[], so the array operation .join() is allowed there.

Going back to our first example, say TextBook can have multiple authors, like this:

type Novel = {
  author: string;
  synopsis: string;
};

type TextBook = {
  author: string[];
  subject: string;
};

type Summary = { author: string; synopsis?: string };

When we try to use typeof for type narrowing

function getSummary(book: Novel | TextBook): Summary {
  if (typeof book.author === "string") {
    return {
      author: book.author,
      synopsis: book.synopsis, // error
    };
  }

  return { author: book.author.join(",") };
}

We still get an error at book.synopsis! But we do not get an error at book.author.join(',')! How can that be?

By doing typeof book.author === 'string', TypeScript only performs type narrowing on book.author. TypeScript does not infer it to the parent object, namely book. In cases like this, we can implement our own type guard with the help of a Type Predicate.

Custom Type Guard with a Type Predicate

A Type Predicate is a TypeScript feature for making an assertion about a type. The syntax for a type predicate is parameter is type, for example book is Novel. A type predicate can only be used as the return type of a function, which turns that function itself into a type guard for its parameter.

For our case, we can create an isNovel function as follows:

function isNovel(book: Novel | TextBook): book is Novel {
  return typeof book.author === "string";
}

With this function, we are telling TypeScript: if isNovel is called with a book object and returns true, then book can be confirmed to be of type Novel. Let’s use our custom type guard function in getSummary.

type Novel = {
  author: string;
  synopsis: string;
};

type TextBook = {
  author: string[];
  subject: string;
};

type Summary = { author: string; synopsis?: string };

function isNovel(book: Novel | TextBook): book is Novel {
  return typeof book.author === "string";
}

function getSummary(book: Novel | TextBook): Summary {
  if (isNovel(book)) {
    return {
      author: book.author,
      synopsis: book.synopsis,
    };
  }

  return { author: book.author.join(",") };
}

Now, TypeScript can precisely determine the data type of book in each block.


There are many more type narrowing techniques in TypeScript, such as the instanceof operator, the never type, equality narrowing, and others. However, the techniques covered here are some that I feel are the most widely used and most practical for everyday TypeScript coding.

When working with complex data types in TypeScript, type narrowing is often the key to breaking through a deadlock. Applying type narrowing techniques will be safer and more robust than simply attaching a type cast with as, or carelessly using any.

Thank you for reading — I hope it is useful!


References and Further Reading