Skip to content

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

Closed

Description

@AnyhowStep

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

Activity

  1. AnyhowStep commented on Aug 23, 2019

    @AnyhowStep
    ContributorAuthor

    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))

  2. ahejlsberg commented on Sep 1, 2019

    @ahejlsberg
    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.

  3. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Sep 1, 2019
  4. AnyhowStep commented on Sep 1, 2019

    @AnyhowStep
    ContributorAuthor

    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.

  5. AnyhowStep commented on Sep 1, 2019

    @AnyhowStep
    ContributorAuthor

    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

  6. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Working as IntendedThe behavior described is the intended behavior; this is not a bug

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions