Skip to content
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

Function overloading breaks type parameter inference for higher-order function #31829

Closed
GregRos opened this issue Jun 8, 2019 · 3 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@GregRos
Copy link

GregRos commented Jun 8, 2019

TypeScript Version: 3.5.1, 3.6.0-dev.20190608, 3.4
Search Terms:
compose, generic unknown, overloaded generic type inference

Code

function test<T, S>(f: (x: T) => S) {
    return f;
}

//                                     V removing this parameter will make the code compile
function pipe<T>(f1: (x: string) => T, str: string): T;
function pipe<T, S>(f1: (x: string) => T, f2: (x: T) => S): S;
function pipe(...args) {
    return null;
}

//                      V x: S is inferred to be unknown here, error
pipe(test(x => x), test(x => x.length));

Expected behavior:

S should be inferred to be string, just like it is without the additional parameter to the first overload of pipe. Or maybe inference should totally fail. But the presence of the extra argument should not change what happens, since overload resolution doesn't seem to be ambiguous here.

Actual behavior:

S is inferred to be unknown, but only in the presence of the extra argument to the first overload. If you remove it, it works perfectly.

Playground Link

The playground uses an older version of tsc, so here x is inferred to be {} instead.
Related Issues:

This - #31738

@ahejlsberg
Copy link
Member

This is effectively a design limitation. During overload resolution the checker has to resolve the types of the x arrow function parameters in the calls to test. The contextual type for the second test call is string in the first overload, so there is nothing from which to infer a type for x and it is given type unknown. Once the x parameters have been assigned types, the checker can resolve the types of the test calls and then realizes the first overload isn't applicable. It then moves on to the second overload, but at this point we are unable to undo the types we have previously assigned to the x parameters (this is the design limitation, we can't change a type once we've assigned it).

I suggest reversing the overloads. It works once you do.

@ahejlsberg ahejlsberg added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jun 10, 2019
@GregRos
Copy link
Author

GregRos commented Jun 11, 2019

Ah I see. It makes sense it wouldn't work then. It's odd that the behavior changes depending on the order of the overloads, but to be honest I didn't think TypeScript would be able to handle this situation at all, so I have to give it props for that.

@GregRos GregRos closed this as completed Jun 11, 2019
@rdhelms
Copy link

rdhelms commented Jan 8, 2020

@ahejlsberg I'd be interested in knowing if the same design limitation that you describe above applies to another case that I'm working on below, or if it might be a different issue:

The main idea is that I'm trying to say "If the type parameter's properties are all optional, then the function's parameter is optional. If the type parameter has required properties, then the function's parameter is required."

And the code below does essentially accomplish that idea, but my complaint is that the error message Type '{ required: string; }' does not satisfy the constraint 'never'. is extremely confusing. What I really would want is an error message of Expected 1 arguments, but got 0.

As far as I can tell, the problem comes from how TS resolves to the first overload based on the absent function parameter, when really I wish that it could resolve to the second overload based on the type parameter. Searching for this led me here, and your comments about overload resolution made me wonder if this is a result of the same design limitation or not.

declare namespace Test {
    type EmptyObject<T> = {} extends T ? {} : never

    interface Thing {
        new <T extends EmptyObject<T>>(params?: T): void
        new <T extends object>(params: T): void
    }
    const Thing: Thing
}

new Test.Thing<{ optional?: number }>() // Optional case - works fine
new Test.Thing<{ optional?: number }>({ optional: 123 }) // Optional case - works fine

new Test.Thing<{ required: string }>({ required: 'hello' }) // Required case - works fine
new Test.Thing<{ required: string }>() // Required case - errors, as expected...although the error is extremely confusing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants