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.

Categories
Uncategorised

Hello world!

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!