As of TS v5.0.0, you can use @overload
in JSDoc which means this document is obsolete.
Yes, you can. No, it's not pretty.
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.
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?
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...
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.