Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 418
Snow Avatar asked Nov 15 '22 18:11

Snow


1 Answers

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'));
}
like image 104
Snow Avatar answered Jun 08 '23 14:06

Snow