Overloaded functions in TypeScript

December 17th 2021 TypeScript

Type checking in TypeScript helps you ensure that your code is correct and behaves as expected. However, in certain scenarios, it seems more difficult to write such code than plain JavaScript, especially if you are not familiar with all the language constructs available to you in TypeScript. Functions that support arguments of multiple types can be one such case.

Let us say we have the following collection of types:

export interface VariantA {
  propertyA: string;
}

export interface VariantB {
  propertyB: string;
}

export interface Wrapper {
  variant: VariantA;
}

Both propertyA and propertyB contain the same information, and depending on the input type, there are three different ways to access it. The following JavaScript function does just that:

function getProperty(input) {
  if (input.variant) {
    input = input.variant;
  }

  if (input.propertyA) {
    return input.propertyA;
  } else if (input.propertyB) {
    return input.propertyB;
  } else {
    return undefined;
  }
}

What is the best way to convert this JavaScript function to TypeScript? The first step would be to declare the type of parameter and return value:

function getProperty(input: any): string | undefined {
  if (input.variant) {
    input = input.variant;
  }

  if (input.propertyA) {
    return input.propertyA;
  } else if (input.propertyB) {
    return input.propertyB;
  } else {
    return undefined;
  }
}

This is better than nothing: the compiler now at least knows what type of value the function returns. But how can we describe the different supported types for the parameter? Function overload signatures are the answer:

function getProperty(input: VariantA): string;
function getProperty(input: VariantB): string;
function getProperty(input: Wrapper): string;
function getProperty(input: any): string | undefined {
  if (input.variant) {
    input = input.variant;
  }

  if (input.propertyA) {
    return input.propertyA;
  } else if (input.propertyB) {
    return input.propertyB;
  } else {
    return undefined;
  }
}

Since JavaScript does not support function overloads, TypeScript simulates this by allowing multiple signatures for a function with only a single implementation. The implementation signature must be a superset of all overload signatures. The implementation signature is hidden from the caller; it can only use one of the three overload signatures provided.

With that we have already provided full typing information to callers, but can we also provide more type safety in the function implementation? We can, by using union types.

function getProperty(input: VariantA): string;
function getProperty(input: VariantB): string;
function getProperty(input: Wrapper): string;
function getProperty(input: VariantA | VariantB | Wrapper): string | undefined {
  if (input.variant) {
    input = input.variant;
  }

  if (input.propertyA) {
    return input.propertyA;
  } else if (input.propertyB) {
    return input.propertyB;
  } else {
    return undefined;
  }
}

This tells the compiler that the parameter in the function implementation is one of the three supported types. However, TypeScript now only allows access to properties that are present in all types. In our case, there are no such properties, so any attempt to access a property is considered an error.

In order for the above function to compile, we need to use type guards:

function getProperty(input: VariantA): string;
function getProperty(input: VariantB): string;
function getProperty(input: Wrapper): string;
function getProperty(input: VariantA | VariantB | Wrapper): string | undefined {
  function isVariantA(value: any): value is VariantA {
    return value.propertyA !== undefined;
  }

  function isVariantB(value: any): value is VariantB {
    return value.propertyB !== undefined;
  }

  function isWrapper(value: any): value is Wrapper {
    return value.variant !== undefined;
  }

  if (isWrapper(input)) {
    input = input.variant;
  }

  if (isVariantA(input)) {
    return input.propertyA;
  } else if (isVariantB(input)) {
    return input.propertyB;
  } else {
    return undefined;
  }
}

The type guard functions isVariantA, isVariantB, and isWrapper tell the compiler that the input parameter is of the specified type if the function returns true. Now I can replace the property checks of the original JavaScript function with these type guard functions. This tells the compiler what type the variable inside the if block is, so I can access the properties of that type there. But it also ensures that I am not accessing any other properties, protecting me from errors in the code.

The code for the function before and after the change, including full test coverage, can be found in my GitHub repository.

TypeScript can provide full type safety, even when the code takes advantage of the dynamic nature of JavaScript. In this post, I presented an example of how function overload signatures, union types, and custom type guards can be used to tell the compiler as much as possible about the types involved.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License