All Articles

Define Multiple Call Signatures for Your Functions in TypeScript with Function Overloads

Joe PreviteJoe Previte

This article will explain function overloads in TypeScript using real-world examples and show you exactly how they’re used. We’ll even include TypeScript playground links for our examples. This way, you can quickly try them yourself. By the end, you’ll know how to use union types like the pros.

What are Function Overloads?

Function overloads are a concept in programming languages that allows you to overload a function with different call signatures. That means you can define the same function in more than one way. Here’s an example from the TypeScript Handbook:

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);

As you can see, the name stays the same, but the number of arguments and the meaning change in each variation. This can be handy if you are writing a function that accepts various types of arguments.

Later, we’ll take a look at some examples from open source and then explain when you should use them.

Show me the code

If you want to jump straight into code examples, take a look at these TypeScript playground links:

Examples of Function Overloads in Open Source

Function overloads are most commonly used in libraries like DOM, lodash, Blitz, and more, to name a few. Here’s an example from Blitz.js (link to source code):

export function useParam(key: string): undefined | string | string[];
export function useParam(key: string, returnType: "string"): string | undefined;
export function useParam(key: string, returnType: "number"): number | undefined;
export function useParam(
  key: string,
  returnType: "array"
): string[] | undefined;
export function useParam(
  key: string,
  returnType?: ReturnTypes
): undefined | number | string | string[] {
  const params = useParams(returnType);
  const value = params[key];

  return value;
}

Let’s break down what’s happening. We have a function called useParam. Key is always required and always a string. Still, returnType can be optional and a variety of different types, even a custom one called ReturnTypes (definition omitted for simplicity but defined here). There are five overload signatures, each with its parameters and return types.

And we can see how it’s used by looking at these tests. Here is one example that I’ve slimmed down:

describe("useParam", () => {
  it("works with number", () => {
    // This is the router query object which includes route params
    const query = {
      id: "1",
    };

    let { result } = renderHook(() => useParam("id", "number"), {
      router: { query },
    });

    expect(result.current).toEqual(1);
  });
});

In this function call, useParam matches this function overload:

export function useParam(key: string, returnType: "number"): number | undefined;

We ask for a param with the key equal to "id", and then we want the return type as "number" to get 1 back. Pretty neat to learn about function overloads and see them used in a real project!

More Use Cases in Open Source

Sometimes it’s helpful to look at other examples when you need inspiration. Since I didn’t have time to break down every example, I leave you with a list:

Shout-out to the TypeScript community on Twitter for helping me crowd-source these examples:

When should you use them?

It depends! Some folks (myself included) believe you can get by without them.

Most of the time, union types solve the problem. As John Reilly points out, they’re also easier to understand and closer to JS. Most of the examples we shared from the open source were from libraries. Therefore, if you’re building a typical production application, you might not need them.

Ordering Matters

If you decide to use them, remember that the order of your overloads matters. Put more specific overloads first:

function isDiv(x: HTMLDivElement): boolean;
function isDiv(x: HTMLElement): boolean;
function isDiv(x: unknown): boolean {
  if (x instanceof HTMLDivElement) {
    return true;
  }
  return false;
}

Here, I order them by specificity: HTMLDivElementHTMLElementunknown. This is because TypeScript uses the first matching overload. Otherwise, it defaults to this “more general” one and never finds the other ones. Read more here.

Resources

For more information on function overloads, take a look at these resources:

Summary

We learned about function overloads and how they can be used to define multiple call signatures for your functions in TypeScript. They’re most often used in libraries like lodash and Redux.

This new knowledge will serve you in understanding private codebases at companies you might work with in the future and open source libraries you might use. Now go out and write some TypeScript!