-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: better type narrowing in overloaded functions through overload set pruning #22609
Comments
Absolutely agreed, this kind of thing would go a long way towards making overloaded functions a lot safer to use. But it might also be a breaking change: overloads can be used to assert/claim type relationships that TS cannot see. Enforcing particular rules may break attempts to use them to circumvent and/or augment those rules in the first place. But then, that usage of overloads always seemed dubious to me... Also, related to #21879, which is requesting a similar thing for conditional types rather than overloads. The same problems raised there may also apply here. |
I'm not sure the problems expressed in #21879 could be a real concern here, as we are not directly infering a generic type instantiation from another. To mirror the example that @DanielRosenwasser gave on #21879: function compare(a: string, b: number): number;
function compare<T>(a: T, b: T): number;
function compare(a, b)
{
switch (typeof a)
{
case "string": // second overload removed, b is number
case "number": // first overload removed, b is T
default: // first overload removed, b is T
}
} As you can see, here we are not infering |
It would be fantastic if we could do something like this: function f(a: number): void;
function f(a: string, b: string): void;
function f(a, b) {
if (typeof a === 'number') {
// `a` is a string and `b` is thus `undefined`
}
} The implementation would not be considered one of the overload signatures, and thus the arguments can be properly narrowed. To preserve compatibility with implicit any, if the feature was only available when |
The solution from @Tiedye basically makes this feature opt-in, which would serve for gradual adoption as people need / want it. |
Here is a type-safe workaround by the time this feature is implemented: type K = string | number | symbol;
const keyIs = <k extends K, C, T extends { [i in k]: unknown; }, V extends { [i in k]: C; }>(
args: T | V, k: k, c: C): args is V => args[k] === c;
type Return<F, arg extends unknown[], R> = F extends { (...args: arg): R; } ? R : never;
function bar(selector: "A", val: number): number;
function bar(selector: "B", val: string): string;
function bar(...args: ["A", number] | ["B", string]): number | string {
if (keyIs(args, 0, "A" as const)) {
let _return: Return<typeof bar, typeof args, number>;
const [selector, val] = args;
const the_selector = selector; // "A"
const the_val = val; // just number
return _return = 42; // when selector is "A" it returns a number, otherwise it raises an error
}
else {
let _return: Return<typeof bar, typeof args, string>;
const [selector, val] = args;
const the_selector = selector; // "B"
const the_val = val; // just string
return _return = "foo"; // when selector is "B" it returns a string, otherwise it raises an error
}
} |
@RyanCavanaugh what was the outcome of the discussions on this? I troll myself regularly wondering why my overload isn't narrowing within my switch statement 😭 |
Really? Two years have passed and the issue is still being ignored? I only see pro here and no one even mentioning any con. |
Control flow analysis for dependent parameters(#47190) is an acceptable workaround if all you're wanting is TS to narrow the types of the args once they match a case. However as far as I can tell you're unable to define a return type for each case. type Bar = (...args: ["A", number] | ["B", string]) => number | string
const bar:Bar = (selector, val) => {
if (selector === "A")
{
const the_selector = selector; // "A"
const the_val = val; // is number
return "foo"; // won't give error unfortunately
}
else
{
const the_selector = selector; // "B"
const the_val = val; // is string
return 42; // won't give error unfortunately
}
} I'd still rather have overloads work as @lodo1995 described, but this works for me in my case as the return type is the same in both cases and I can avoid this craziness: return (val as number).toFixed(2); |
Until the feature is implemented, the cleanest solution is to use a discriminated union type. Example with overloads ( type Food = 'TANGERINE' | 'PASTA';
type Cutlery = 'FORK' | 'SPOON';
function eat(food: 'TANGERINE'): void;
function eat(food: 'PASTA', cutlery: Cutlery): void;
function eat(food: Food, cutlery?: Cutlery) {
if (food === 'TANGERINE') cutlery;
else cutlery;
}
eat('TANGERINE'); Example with discriminated union ( type Food = 'TANGERINE' | 'PASTA';
type Cutlery = 'FORK' | 'SPOON';
type Meal = { food: 'TANGERINE'; cutlery: never } | { food: 'PASTA'; cutlery: Cutlery };
function eat({ food, cutlery }: Meal) {
if (food === 'TANGERINE') cutlery;
else cutlery;
}
eat({ food: 'TANGERINE' } as Meal); The only "drawback" is that the function now expects a single object containing all the arguments. EDIT: It turns out this method is not clean and reliable. |
To avoid making that wall of a comment even longer, I post this useful information here. The problems outlined at the end of my previous comment can be fixed by making the type Food = 'TANGERINE' | 'PASTA';
type Cutlery = 'FORK' | 'SPOON';
type Meal = { food: 'TANGERINE'; cutlery?: never } | { food: 'PASTA'; cutlery: Cutlery };
function eat({ food, cutlery }: Meal) {
if (food === 'TANGERINE') cutlery;
else cutlery;
}
eat({ food: 'TANGERINE' });
eat({ food: 'PASTA', cutlery: 'FORK'});
eat({ food: 'TANGERINE', cutlery: 'SPOON' }); // error
eat({ food: 'PASTA' }); // error I feel like I'm spamming, but I trust that one day this will be helpful to someone. |
The pattern encouraged here as a work around has very poor performance characteristics for runtime code and basically should not be used for hot-paths. The allocation of extra wrapper objects so that we can narrow off of a union of interfaces pays a double cost, both for the allocation but also for the inevitable GC interruption. |
@runspired I totally agree, it's always a shame to sacrifice runtime performance to address TS shortcomings. |
Is the following a problem resulting from this issue, or is it something else? interface TestType {
m1(): string;
m1(arg1: number): number;
}
function mytest(obj: TestType, testNumber?: number) {
obj.m1(testNumber);
} As you can see in the Playground example, we have an error when calling But |
No, it’s not this problem or any TS problem—you misunderstand what your code is saying.
False. It does not have any overload that accepts Imagine this situation: const foo: TestType = {
m1: function() {
if (arguments.length === 1) return 'not a circle';
const arg1: number = arguments[0];
return Math.pow(arg1, 2) * Math.PI;
},
}; (This is a terrible implementation of this signature, and you should almost never use According to your interface, But if you call In short, The correct way to write interface TestType {
m1(arg1?: undefined): string;
m1(arg1: number): number;
} Now it’s clear that we can’t rely on the number of arguments to determine the type of (Also, for the record, this signature is weird, and probably a code smell. Even with my improved version of the signature, Typescript won’t recognize any function as having that signature—to assign |
@krryan Thank you for the clarification. That makes much more sense now. As for the signature, it is a simplified version of what I found in the crypto module from NodeJS |
Just chiming in to mention what won't work and mention that I recently also trolled myself by thinking overloading/workaround I thought would work could narrow type. We have a function that takes two arguments, if (src instanceof CallerPort && dst instanceof CalleePort) {
// do something
} else if (src instanceof IOPort && dst instanceof IOPort) {
// do something else
} else { // error handling
} Wishing to make it a switch-case and eliminate the possibility of passing in non-matching arguments, I changed the function signature to be public connect<R, S extends R>(src: IOPort<S>, dst: IOPort<R>): void;
public connect<A extends T, R, T, S extends R>(
src: CallerPort<A, R>,
dst: CalleePort<T, S>
): void;
public connect<A extends T, R, T, S extends R>(
src: CallerPort<A, R> | IOPort<S>,
dst: CalleePort<T, S> | IOPort<R>
): void But in the implementation it won't narrow type. Consequently I tried this modified version of a workaround: public connect<A extends T, R, T, S extends R>(
...args: [CallerPort<A, R>, CalleePort<T, S>] | [IOPort<S>, IOPort<R>]
): void {
const [src, dst] = args; But inside of This really isn't a huge problem for us as it's really 3 lines of additional code, but I really do think this would be a wonderful feature to add. |
1. Implemented ConnectablePort that can be used as `Variable` 2. Refactored ConnectablePort with function overloading at the same time to facilitate better type check (but due to TS limitation there's no type narrowing, see microsoft/TypeScript#22609) 3. Made tests conform to the new connection API
1. Implemented ConnectablePort that can be used as `Variable` 2. Refactored ConnectablePort with function overloading at the same time to facilitate better type check (but due to TS limitation there's no type narrowing, see microsoft/TypeScript#22609) 3. Made tests conform to the new connection API
1. Implemented ConnectablePort that can be used as `Variable` 2. Refactored ConnectablePort with function overloading at the same time to facilitate better type check (but due to TS limitation there's no type narrowing, see microsoft/TypeScript#22609) 3. Made tests conform to the new connection API
1. Implemented ConnectablePort that can be used as `Variable` 2. Refactored ConnectablePort with function overloading at the same time to facilitate better type check (but due to TS limitation there's no type narrowing, see microsoft/TypeScript#22609) 3. Made tests conform to the new connection API
1. Implemented ConnectablePort that can be used as `Variable` 2. Refactored ConnectablePort with function overloading at the same time to facilitate better type check (but due to TS limitation there's no type narrowing, see microsoft/TypeScript#22609) 3. Made tests conform to the new connection API
TypeScript Version: 2.7.2
Search Terms: type inference, type narrowing, overloads
Code
Issue
The snippet above shows a limitation of the current type inference implementation in TypeScript.
As can be clearly seen from the overloaded declarations, when
selector === "A"
we haveval: number
and we must return anumber
, while whenselector === "B"
we haveval: string
and we must return astring
. But the type-checker, although able to understand thatthe_selector
can only be"A"
in theif
block and only"B"
in theelse
block, still typesthe_val
asnumber | string
in both blocks and allows us to return astring
whenselector === "A"
and anumber
whenselector === "B"
.Possible Solution
In the implementation of an overloaded function, when the type-checker decides to narrow the type of a parameter (as often happens in
if-else
blocks), it should also remove the overloads that don't match the narrowed parameter type from the overload set, and use the pruned overload set to narrow the types of the other parameters.Playground Link: https://www.typescriptlang.org/play/#src=%0Afunction%20bar(selector%3A%20%22A%22%2C%20val%3A%20number)%3A%20number%3B%0Afunction%20bar(selector%3A%20%22B%22%2C%20val%3A%20string)%3A%20string%3B%0Afunction%20bar(selector%3A%20%22A%22%20%7C%20%22B%22%2C%20val%3A%20number%20%7C%20string)%3A%20number%20%7C%20string%0A%7B%0A%20%20%20%20if%20(selector%20%3D%3D%3D%20%22A%22)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20const%20the_selector%20%3D%20selector%3B%0A%20%20%20%20%20%20%20%20const%20the_val%20%3D%20val%3B%0A%20%20%20%20%20%20%20%20return%20%22foo%22%3B%0A%20%20%20%20%7D%0A%20%20%20%20else%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20const%20the_selector%20%3D%20selector%3B%0A%20%20%20%20%20%20%20%20const%20the_val%20%3D%20val%3B%0A%20%20%20%20%20%20%20%20return%2042%3B%0A%20%20%20%20%7D%0A%7D
The text was updated successfully, but these errors were encountered: