I'd like to define a function which can accept parameters typed in one of two ways. For example:
type Fn = {
(abc: number, def: string): void,
(abc: string): void,
};
Given this type signature, if abc
is a number, then def
is a string, and if abc
is a string, then def
is not defined. This is clear to humans, but is there any way for Typescript to recognize it? For example, the following implementation fails:
const fn: Fn = (abc: number | string, def?: string) => {
if (typeof abc === 'string') console.log(abc.includes('substr'));
else console.log(def.includes('substr'));
}
because although the type of abc
has been narrowed, TS doesn't understand that the type of def
has been determined too, so def.includes
is not permitted. The grouping of argument types is recognized for callers of the function, so the following is forbidden, as expected:
fn('abc', 'def');
But the overloaded type grouping seems to have no effect inside the function.
When there are only a couple of parameters, it's easy enough to explicitly (and redundantly) type-check each parameter, or use a type assertion for each once one has been checked, but that's still ugly. It gets much worse when there are more than a couple of parameters.
Another problematic redundancy is that each possible argument type needs to be listed not only in the type
, but also in the function's parameter list. Eg (abc: number)
and (abc: string)
in the type definition also requires = (abc: number | string)
in the parameter list.
Is there a better pattern available for function overloading without ditching it entirely? I know of at least two workarounds that don't involve overloading:
Pass an object of type { abc: number, def: string } | { abc: string }
instead of multiple separate parameters, then pass the object go through a type-guard
Use two separate functions for the two different types of parameters
But I'd rather use overloading if there's a decent way to handle it.
This is now possible to a limited extent with TypeScript 4.6's Control Flow Analysis for Dependent Parameters.
TypeScript can narrow down the types of arguments if the narrowing is done with ===
. (A typeof check doesn't appear to work, at least not yet.)
To tweak the example in the question, if the first argument, when it is a string, is abc
, has no second argument, that can be detected and narrowed by TypeScript thusly:
type Fn = (...args: [number, string] | ['abc', undefined]) => void;
const fn: Fn = (abc, def) => {
if (abc === 'abc') console.log(abc.includes('substr'));
else console.log(def.includes('substr'));
}
The narrowing of the first argument to abc
allows TS to infer that in the else
, the second argument is a string, and not undefined
.
It'd be great if types wider than literals could use this same technique, but they don't work (yet) - the typeof
check on the first argument fails to narrow the second argument.
type Fn = (...args: [number, string] | [string, undefined]) => void;
const fn: Fn = (abc, def) => {
if (typeof abc === 'string') console.log(abc.includes('substr'));
else console.log(def.includes('substr'));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With