Skip to content

Instantly share code, notes, and snippets.

@boneskull
Last active October 23, 2023 22:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boneskull/db92554a4da27c4905f0321d571ba68c to your computer and use it in GitHub Desktop.
Save boneskull/db92554a4da27c4905f0321d571ba68c to your computer and use it in GitHub Desktop.
TypeScript Function Overloads in PURE JAVASCRIPT!!!!

UPDATE

As of TS v5.0.0, you can use @overload in JSDoc which means this document is obsolete.

How to Overload Functions using TypeScript Types in JavaScript

Summary

Yes, you can. No, it's not pretty.

The Situation

In TypeScript, you can overload functions, like so:

function foo(bar: string): number;
function foo(bar: number): string;
function foo(bar: string|number) {
  if (typeof bar === 'string') {
    return 1;
  }
  return '1';
}

Try as you might, that's just not gonna fly in JavaScript.

First Attempts

I first encountered this a year or two ago, and thought that perhaps using a conditional type as my function's return type could solve this. So, let's try that, rewriting the above TS source in JS:

Note: We're checking our JS in TS' strict mode here.

/**
 * @template {string|number} B
 * @param {B} bar
 * @returns {B extends string ? number : string}
 */
function foo(bar) {
  if (typeof bar === 'string') {
    return 1; // ERROR
  }
  return '1'; // ERROR
}

What happens when TS sees this? It produces two (2) errors: Type 'number' is not assignable to type 'B extends string ? number : string'.(2322) and ype 'string' is not assignable to type 'B extends string ? number : string'.(2322).

In fact, the following TS raises the same errors:

function foo<B extends string | number>(bar: B): B extends string ? number: string {
  if (typeof bar === 'string') {
    return 1; // ERROR
  }
  return '1'; // ERROR
}

And no, extracting the return type into its own type doesn't help:

type FooResult<B extends string | number> = B extends string ? number : string;

function foo<B extends string | number>(bar: B): FooResult<B> {
  if (typeof bar === 'string') {
    return 1; // ERROR
  }
  return '1'; // ERROR
}

This behavior suggests to me that conditional types as return types won't get the job done. In fact, AFAICT, they do not work at all and should be avoided (if you have evidence to the contrary, please share!).

So conditional types are out. What can we do instead?

An Aside: Function Overloads in Object Literals

In TS, you can use a function overload on a function declaration, within a class body, and in an interface (and maybe others). One place you can't use them is in object literals:

// this just results in a syntax error

const FooBlob = {
  foo(bar: string): number;
  foo(bar: number): string;
  foo(bar: string|number) {
    if (typeof bar === 'string') {
      return 1;
    }
    return '1';
  }
};

To work around this, you can use function declarations (as in the first example) and just stuff the function into the object:

function foo(bar: string): number;
function foo(bar: number): string;
function foo(bar: string | number) {
  if (typeof bar === 'string') {
    return 1;
  }
  return '1';
}

const FooBlob = {
  foo
};

FooBlob.foo(1) // '1'
FooBlob.foo('1') // 1

You may want to try to make FooBlob implement some interface, like so:

interface IFooBlob {
  foo(bar: number): string;
  foo(bar: string): number;
}

const FooBlob: IFooBlob = {
  foo(bar: string | number ) {
    if (typeof bar === 'string') {
      return 1;
    }
    return '1';
  }
}

Unfortunately, that doesn't work; you get this lovely thing:

Type '(bar: string | number) => 1 | "1"' is not assignable to type '{ (bar: string): number; (bar: number): string; }'.
  Type 'string | number' is not assignable to type 'number'.
    Type 'string' is not assignable to type 'number'.(2322)

AFAICT, the only way one can implement an interface that contains overloads is via a class. Am I wrong?

One of the things I tried--which didn't work for the "object literal" case--was this intersection:

interface IFooBlob {
  foo: ((bar: string) => number) & ((bar: number) => string)   
}

I'm not sure how the above is different from using overloads. It seems to work the same way. That gives me an idea...

Man-Made Horrors, Etc., Etc.

A Type Assertion allows you to nudge the TS compiler in a particular direction.

As far as I can tell, the type assertion must be plausible to the compiler; i.e. related in some way to whatever the compiler infers the value is.

So what if we made two types: one for (bar: string) => number) and another for (bar: number) => string). We could use a type assertion on a function expression (this is important) and maybe it'd work?

In JS, type assertions must wrap the "target" in parens. And AFAICT, wherever this is legal in JS, a type assertion is legal.

For those who don't know, @callback is sugar for a @typedef which describes a function.

/**
 * @callback FooA
 * @param {string} bar
 * @returns {number}
 */

/**
 * @callback FooB
 * @param {number} bar
 * @returns {string}
 */

const foo = /** @type {FooA & FooB} */ (function (bar) {
 if (typeof bar === 'string') {
    return 1;
  }
  return '1';
});

foo(1) // '1'
foo('1') // 1

The above does not cause the compiler to throw an error. AFAICT, this is how you express overloaded functions in JS.

Is there another way? Please share.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment