Categories
Typescript Terrors Uncategorised

Typescript Overloading

I spent a couple of hours recently trying to solve a problem with overloading in TypeScript only to realise there wasn’t actually a problem at all – I had just completely misunderstood how overloading works !

Unlike other languages where an overload acts as an “alias” for the implementing method, in TypeScript an overload “replaces” / hides the implementing method – the implementing method becomes inaccessible unless called via an overload signature.

Example

I’ll try to show a basic example and then go into more details.

Imagine, for the sake of this example, an equivalent of “parseInt” (that we could call doParseInt) that takes either a number or a string.

  • If a number is passed we return the number directly.
  • If a string is passed however, a second argument “radix” is required.

We could code this like:

// Basic function to test overloading: can be called in two ways
// value is number (radix not used)
// value is string (radix required)

function doParseInt(value: (number | string), radix?: number): number {
  if (typeof value === 'number') {
    return value;
  }
  if (typeof value === 'string') {
    return parseInt(value, radix);
  }
  throw new Error(`unexpected value: ${value}`);
}

We can use it like:

const val1 = doParseInt(123); // OK
const val2 = doParseInt('123'); // Error - radix is required if value is string
const val3 = doParseInt(123, 8); // Error - radix is not used if value is number
const val4 = doParseInt('123', 10); // OK
const val5 = doParseInt(123 as (number | string)); // OK

This works but is not great from a type-safety point of view – we have no way of forcing to people to pass a radix if value is string, or likewise from not passing a radix if value is numeric.

Overloading to the rescue – We add our overload signatures to refine the way we can call the implementing function:

function doParseInt(value: number): number;
function doParseInt(value: string, radix: number): number;

Note these are not “functions” – they do not have a body!

They serve purely as “overload signatures” – method signatures that refine (limit) how we can call the implementing method.

After adding our signatures, everything seems great except “val5” which no longer compiles – it appears the fact of adding an overload signature has made the original implementing method inaccessible !

const val1 = doParseInt(123); // OK
// const val2 = doParseInt('123'); // No longer compiles - great
// const val3 = doParseInt(123, 8); // Likewise, great
const val4 = doParseInt('123', 10); // OK
const val5 = doParseInt(123 as (number | string)); // No longer compiles either .... !

Results in:

error TS2345: Argument of type 'string | number' is not assignable to parameter of type 'number'.
Type 'string' is not assignable to type 'number'.

The problem comes from val5 – we want to be able to call directly the non-overloaded implementation method.

However, this is not possible in TypeScript – adding a single overload makes the non-overloaded implementation inaccessible.

To resolve this, we need to simply add the original method signature as an overload signature of itself.

The final code looks like:

function doParseInt(value: number): number;

function doParseInt(value: string, radix: number): number;

function doParseInt(value: (number | string), radix?: number): number;

// Basic function to test overloading: can be called in two ways
// value is number (radix not used)
// value is string (radix required)
function doParseInt(value: (number | string), radix?: number): number {
  if (typeof value === 'number') {
    return value;
  }
  if (typeof value === 'string') {
    return parseInt(value, radix);
  }
  throw new Error(`unexpected value: ${value}`);
}

In hindsight it seems clearly logical – we add overload signatures to refine/specialise the call, and so it doesn’t make much sense to leave the original implementing method available.

What would have been great would have been an error message more explicit: Implementing function not available if overload signature exists” for example

This example is slightly contrived: The actual problem was with array signatures (RxJS combineLatest for example) but that will be the subject of my next post.