Skip to content

Passing a constrained generic type to an overloaded function with overlapping parameter types yields incorrect return type #13223

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

Closed
rotemdan opened this issue Dec 30, 2016 · 4 comments

Comments

@rotemdan
Copy link

rotemdan commented Dec 30, 2016

TypeScript Version: Latest nightly

Code

function func1(a: number[]): number;
function func1(a: any[]): string;
function func1(a: any[]): number | string {
	// ...
}

function func2<T extends any[]>(b: T) {
	return func1(b);
}

const result = func2([1, 2, 3, 4]); // `result` is incorrectly inferred as `string`

Expected behavior:
Shouldn't compile. The generic type T cannot be conclusively determined not to be a number[].

Actual behavior:
result gets the incorrect return value string, instead of the expected number.

Notes:
To clarify, the semantics of function func2<T extends any[]>(b: T) should be different from function func2(b: any[]). A generic type should be seen as undetermined and treated like a "black box". The constraint T extends any[] doesn't mean that T can be treated as interchangeable with any[] at all cases.

This is very likely to be labeled as a "design limitation" (though I can't see any reason why it couldn't be fixed) but I wanted to file this to make sure it is understood and documented somewhere.

You can safely close the issue once you've read it, if you wish. It is also possible to fix the issue by disallowing the binding of overlapping overloaded parameter types (I mean, not disallowing in general) when constrained generics are passed to them. The backwards-compatibility impact would most likely be relatively small and fixing it would allow more bugs to be caught. It's up to you to decide.

@rotemdan
Copy link
Author

rotemdan commented Dec 30, 2016

Another way to address this is to infer func1's return type as the union of all the return types of overloads with a parameter a who's type is a subtype of the generic constraint (here any[]):

function func1(a: number[]): number;
function func1(a: boolean[]): boolean;
function func1(a: any[]): string;
function func1(a: string): Date;
function func1(a: any[] | string): number | boolean | string | Date {
	// ...
}

function func2<T extends any[]>(b: T) {
	return func1(b);
}

const result = func2([1, 2, 3, 4]); // `result` is inferred as `number | boolean | string`

though I'm not sure, at this point, if that's really the "right" thing to do.

Edit: if the function has more than a single parameter, this solution may create a challenge for figuring out what to expect from further arguments (e.g. func(b, c, d)), it isn't clear which overload should be matched?

@rotemdan
Copy link
Author

rotemdan commented Dec 30, 2016

The funny thing is that in practice the same problem happens even if the parameter is simply annotated as any[] (no generics involved):

function func1(a: number[]): number;
function func1(a: any[]): string;
function func1(a: any[]): number | string {
	// ...
}

function func2(b: any[]) {
	return func1(b);
}

const result = func2([1, 2, 3, 4]); // `result` is incorrectly inferred as `string` but the 
                                    //  actual type, at runtime is `number`

Since any[] can hold an array of any type, having any additional, more specific array type (e.g. number[], string[] etc..) as an overloaded parameter type would always cause an errornous inferred type if the actual runtime type of the argument (which is only annotated as any[]) matches that overload (assuming the return types of the overloads are different).

This scenario can happen practically in every case where a type and a subtype are both annotated as the overload types of a parameter and the overloads have different return types.

@rotemdan
Copy link
Author

rotemdan commented Dec 31, 2016

My personal thoughts:

I don't think the problem here is really a lack of "soundness", because the scenario that is described consistently and deterministically yields an incorrectly inferred type even given very reasonable circumstances. It is not that "occasionally" it happens that the underlying runtime type matches the more specific overload. It is almost certain to happen since that type is a valid parameter type for the function.

I'll try to be (sincerely) emphatic though, this is very unfortunate. I just hope this pattern isn't frequently used in real code though, and if it does, then programmers must know about it and how to avoid it.

I would even suggest a having a compiler switch that disallows overlapping types in overloads entirely, except special circumstances, say when the return types match exactly or the more general parameter type's return type is a supertype or the more specific one. A linter rule could also help, though its output might get buried in hundreds of warnings about semicolons and brackets.

@rotemdan
Copy link
Author

rotemdan commented Dec 31, 2016

I'm closing this issue and continuing it in a new issue #13235 that targets the more general case I described later in the comments.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant