问题
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-guardUse 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.
回答1:
All of your approaches can be reasonable: (1) separate function declarations (2) union of object parameters or (3) a function overload like in question.
I would prefer (1), If the caller already has enough information to decide which function has to be called, as this reduces the overall conditional complexity of fn
body.
(2) makes more sense with a discriminated union type, so you don't loose excess property checks for the caller. Example:
type OverloadParam =
| { kind: "a", abc: number; def: string }
| { kind: "b"; abc: string }
type Fn = (arg: OverloadParam) => void
const fn: Fn = (args) => {
if (args.kind === "a") {
args.def.includes('substr')
} else {
args.abc.includes('substr')
}
}
In addition, you don't have to list the types twice both in Fn
and as part of fn
signature. I found this oldie here as a reason: only functions with a single overload can apply a contextual type.
With (3) there is no clever way to handle variable function parameters inside the function. TS/JS don't support a function overload implementation like this:
function fn(abc: number, def: string): void { }
function fn(abc: string): void { }
// error (TS): Duplicate function implementation. JS would overwrite the first declaration
So you will always have to use a wider type signature with optional parameters and/or union types and narrow these types down inside the function body.
来源:https://stackoverflow.com/questions/59745527/is-it-possible-to-narrow-the-types-of-overloaded-parameters-without-exhaustively