Is it possible to narrow the types of overloaded parameters without exhaustively checking each parameter in the function body?

大兔子大兔子 提交于 2021-01-27 19:57:03

问题


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.


回答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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!