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.