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

ReturnType<F> in parameter of generic function breaks inference when F has one implicitly typed param #33042

Closed
AnyhowStep opened this issue Aug 23, 2019 · 4 comments
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Aug 23, 2019

TypeScript Version: 3.5.1

Search Terms:

  • ReturnType
  • Implicit type annotation
  • Generic function
  • Parameter

Code

This is me opening a new issue for #29133

I have three separate code snippets, please bear with me =(

//Innocent enough.
//Returns a T
type Callback<T> = (...args : any[]) => T;

declare function noReturnTypeInParam<
    F extends Callback<3|5>
>(f: F): ReturnType<F>

//Expected: const noArg: 3
//Actual  : const noArg: 3
const noArg = noReturnTypeInParam(
    //Return type is 3 
    () => 3
);
//Expected: const withArg: 3
//Actual  : const withArg: 3
const withArg = noReturnTypeInParam(
    //Return type is 3 
    t => 3
);

Playground

//Innocent enough.
//Returns a T
type Callback<T> = (...args : any[]) => T;

//Forgive the contrived example
declare function withReturnTypeInParam<
    F extends Callback<3|5>,
>(
    f: F & (
        ReturnType<F> extends 3 ?
        unknown :
        [
            "Only 3 allowed, received",
            ReturnType<F>
        ]
    )
): ReturnType<F>

//Expected: const noArg: 3
//Actual  : const noArg: 3
const noArg = withReturnTypeInParam(
    //Return type is 3 
    () => 3
);
//Expected: const withArg: 3
//Actual  : const withArg: 3|5
const withArg = withReturnTypeInParam(
    /*
        Expected: No error
        Actual  :
            Argument of type
            '(t: any) => 3'
            is not assignable to parameter of type
            'Callback<3 | 5> & ["Only 3 allowed, received", 3 | 5]'.
    */
    //Return type is `3`
    //function(t: any): 3
    t => 3
);

//Expected: const withArgAndExplicitReturnTypeAnnotation: 3
//Actual  : const withArgAndExplicitReturnTypeAnnotation: 3|5
const withArgAndExplicitReturnTypeAnnotation = withReturnTypeInParam(
    /*
        Expected: No error
        Actual  :
            Argument of type
            '(t: any) => 3'
            is not assignable to parameter of type
            'Callback<3 | 5> & ["Only 3 allowed, received", 3 | 5]'.
    */
    //We *force* the return type to be 3
    //Return type is `3`
    //function(t: any): 3
    (t) : 3 => (3 as 3)
);

//Expected: const withExplicitlyTypedArg: 3
//Actual  : const withExplicitlyTypedArg: 3
const withExplicitlyTypedArg = withReturnTypeInParam(
    /*
        Expected: No error
        Actual  : No error
    */
    //Return type is `3`
    //function(t: any): 3
    (t : any) => 3
);

Playground

//Innocent enough.
//Returns a T
type Callback<T> = (...args : any[]) => T;

//Forgive the contrived example
declare function returnTypeAsTypeParam<
    T extends 3|5,
>(
    f: Callback<
        & T
        & (
            T extends 3 ?
            unknown :
            [
                "Only 3 allowed, received",
                T
            ]
        )
    >
): T

//Expected: const noArg: 3
//Actual  : const noArg: 3
const noArg = returnTypeAsTypeParam(
    //Return type is 3 
    () => 3
);
//Expected: const withArg: 3
//Actual  : const withArg: 3
const withArg = returnTypeAsTypeParam(
    /*
        Expected: No error
        Actual  : No error
    */
    //Return type is `3`
    //function(t: any): 3
    t => 3
);

//Expected: const withArgAndExplicitReturnTypeAnnotation: 3
//Actual  : const withArgAndExplicitReturnTypeAnnotation: 3
const withArgAndExplicitReturnTypeAnnotation = returnTypeAsTypeParam(
    /*
        Expected: No error
        Actual  : No error
    */
    //We *force* the return type to be 3
    //Return type is `3`
    //function(t: any): 3
    (t) : 3 => (3 as 3)
);

Playground

Related Issues:

#29133

#32540 (comment)


I hope this is concise enough ><
I've commented all the relevant locations with what is expected and what actually happens.

Snippets 2 (withReturnTypeInParam) and 3 (returnTypeAsTypeParam) are expected to have the same behaviour because they mean the same thing semantically (to me).

They are just expressed differently syntactically.

I just have trouble understanding why snippet 2 breaks unless I have an explicit type annotation. I'm 99% sure it has to be a bug.

And I have trouble understanding why snippet 3 is OK

@AnyhowStep
Copy link
Contributor Author

There's a reason I want to use snippet 2 and not snippet 3,
#32442

For snippet 2,

//Innocent enough.
//Returns a T
type Callback<T> = (...args : any[]) => T;

//Forgive the contrived example
declare function withReturnTypeInParam<
    F extends Callback<3|5>,
>(
    f: F & (
        ReturnType<F> extends 3 ?
        unknown :
        [
            "Only 3 allowed, received",
            ReturnType<F>
        ]
    )
): ReturnType<F>

declare function withReturnTypeInParam2<
    F extends Callback<3|5>,
>(
    f: F & (
        ReturnType<F> extends 3 ?
        unknown :
        [
            "Only 3 allowed, received",
            ReturnType<F>
        ]
    )
): ReturnType<F>

let x = withReturnTypeInParam;
//Expected: OK
//Actual  : OK
x = withReturnTypeInParam2;

Playground

For snippet 3,

//Innocent enough.
//Returns a T
type Callback<T> = (...args : any[]) => T;

//Forgive the contrived example
declare function returnTypeAsTypeParam<
    T extends 3|5,
>(
    f: Callback<
        & T
        & (
            T extends 3 ?
            unknown :
            [
                "Only 3 allowed, received",
                T
            ]
        )
    >
): T
declare function returnTypeAsTypeParam2<
    T extends 3|5,
>(
    f: Callback<
        & T
        & (
            T extends 3 ?
            unknown :
            [
                "Only 3 allowed, received",
                T
            ]
        )
    >
): T
let x = returnTypeAsTypeParam;
/*
    Expected: OK
    Actual:
        Type 
        '<T extends 3 | 5>(f: Callback<T & (T extends 3 ? unknown : ["Only 3 allowed, received", T])>) => T' 
        is not assignable to type 
        '<T extends 3 | 5>(f: Callback<T & (T extends 3 ? unknown : ["Only 3 allowed, received", T])>) => T'. 
        Two different types with this name exist, but they are unrelated.
*/
x = returnTypeAsTypeParam2;;

Playground

The conditional type must be in the return type of Callback<>.
We want Callback<T & /*myConditionalType*/>.
We do not want Callback<T> & /*myConditionalType*/ because I have seen it break type inference (especially over here: #32540 (comment))

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Aug 23, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.7.0 milestone Aug 23, 2019
@ahejlsberg
Copy link
Member

This is working as intended. In the call withReturnTypeInParam(t => 3), the t parameter is contextually typed. This means we defer inferences from the t => 3 argument and first infer from other arguments such that we can produce the best possible contextual type. However, there are no other arguments, so we make no inferences for F. Then, the use of ReturnType<F> in the contextual parameter type causes us to apply the inferences we've made so far, and since there aren't any we default to the constraint 3 | 5. That subsequently produces an incompatible parameter type.

The core issue here is that you can't simultaneously infer from an argument and depend on the argument.

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Sep 1, 2019
@ahejlsberg ahejlsberg removed this from the TypeScript 3.7.0 milestone Sep 1, 2019
@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Sep 1, 2019

Doesn't returnTypeAsTypeParam basically do the same thing as withReturnTypeInParam, but in a different way?

What's the reason returnTypeAsTypeParam works? Can I rely on it working in the future as well?


Wait-
I'm guessing it's because it doesn't have to infer the type of t for returnTypeAsTypeParam. So, we don't run into that problem.

That makes sense.

@AnyhowStep
Copy link
Contributor Author

I guess I'll close this issue.

The core issue here is that you can't simultaneously infer from an argument and depend on the argument

I have seen cases where TS is able to infer the type of the argument, depend on it, and also correctly infer the ReturnType<>

But it probably only worked in those cases out of dumb luck, I guess

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants