Skip to content

[Regression] ReturnType<> Inference works on 3.3.3, breaks on 3.4.1, 3.5.1 #31814

Closed

Description

@AnyhowStep

TypeScript Version: 3.4.1, 3.5.1

Search Terms: regression, ReturnType, parameter, inference

Code

interface Mapper<HandledInputT, OutputT> {
    (name : string, mixed : HandledInputT) : OutputT,
}


export type IsPipeable<
    FromF extends Mapper<any, any>,
    ToF extends Mapper<any, any>
> = (
    ToF extends Mapper<infer HandledInputT, any> ?
    (
        ReturnType<FromF> extends HandledInputT ?
        true :
        false
    ) :
    false
);

export type AssertPipeable<
    FromF extends Mapper<any, any>,
    ToF extends Mapper<any, any>
> = (
    IsPipeable<FromF, ToF> extends true ?
    ToF :
    [ReturnType<FromF>, "is not pipeable"]
);

declare function pipe<
    F0 extends Mapper<unknown, any>,
    F1 extends Mapper<any, any>
> (
    f0 : F0,
    f1 : AssertPipeable<F0, F1>
): void;

declare function literal<ArrT extends (string | number | boolean | bigint | null | undefined)[]>(
    ...arr: ArrT
): (
    Mapper<unknown, ArrT[Extract<keyof ArrT, number>]>
);
declare const b: Mapper<"0" | "1" | 0 | 1 | "false" | "true", boolean>

//TS suddenly thinks the first arg is Mapper<unknown, any>.
//It is actually Mapper<unknown, "0" | "1" | 0 | 1 | "false" | "true">.
pipe(
    //TS knows this is Mapper<unknown, "0" | "1" | 0 | 1 | "false" | "true">
    literal("0", "1", 0, 1, "false", "true"),
    /*
        Expected: works.
        Actual:
        Argument of type 'Mapper<0 | "0" | "1" | 1 | "false" | "true", boolean>'
        is not assignable to parameter of type '[any, "is not pipeable"]'.

        Succeeds with 3.3.3,
        Fails with 3.4.1,
        Fails with 3.5.1
    */
    b
);

const a = literal("0", "1", 0, 1, "false", "true");
//TS now knows the first arg is Mapper<unknown, "0" | "1" | 0 | 1 | "false" | "true">
pipe(
    a,
    /*
        This works.
        `a` is literally the same type as
        `literal("0", "1", 0, 1, "false", "true")`.

        Why would the first pipe() call fail,
        but the second one succeed?

        Succeeds with 3.3.3,
        Succeeds with 3.4.1,
        Succeeds with 3.5.1
    */
    b
);

Expected behavior:

The below should work,

pipe(
    literal("0", "1", 0, 1, "false", "true"),
    b
);

Actual behavior:

It used to work but does not work now.

Playground Link: Playground

Related Issues:

I feel like it is related to #29133 somehow. Because I have ReturnType<> being used in a parameter again. But it's somewhat different this time in that the first argument is not an anonymous function "literal" I created. It's a function given by another function.

Activity

  1. AnyhowStep commented on Jun 7, 2019

    @AnyhowStep
    ContributorAuthor

    This is a simplified repro. My actual use case is more complicated in that IsPipeable<> has way more rules. It's not as simple as "Output of A must match input of B" because pipe() may do other things behind the scenes to make the data acceptable to B, or B may have additional properties that make it accept a larger range of inputs than its parameters may suggest.

    So, the repro might not make much sense here =x

  2. RyanCavanaugh commented on Jun 25, 2019

    @RyanCavanaugh
    Member

    Anders Hejlsberg (@ahejlsberg) looks like another instance of contextual return type inference messing things up, though it's not completely obvious how in this example

  3. ahejlsberg commented on Jul 1, 2019

    @ahejlsberg
    Member

    This is an effect of #30215 and I'm not sure there's much we can do to change it. With #30215 we sometimes defer processing of arguments in order to make better inferences (but we didn't before 3.4.1). This is captured here in my comment in resolveCallExpression:

    When a call to a generic function is an argument to an outer call to a generic function for which inference is in process, we have a choice to make. If the inner call relies on inferences made from its contextual type to its return type, deferring the inner call processing allows the best possible contextual type to accumulate. But if the outer call relies on inferences made from the return type of the inner call, the inner call should be processed early. There's no sure way to know which choice is right (only a full unification algorithm can determine that), so we resort to the following heuristic: If no type arguments are specified in the inner call and at least one call signature is generic and returns a function type, we choose to defer processing. This narrowly permits function composition operators to flow inferences through return types, but otherwise processes calls right away. We use the resolvingSignature singleton to indicate that we deferred processing. This result will be propagated out and eventually turned into nonInferrableType (a type that is assignable to anything and from which we never make inferences).

    In the pipe(literal(...), b) example, we defer processing of the literal(...) call for the reasons explained above. This means we initially make no inferences for F0, which in turn means the AssertPipeable conditional type isn't happy. Basically, the way the pipe function is declared, type inference only succeeds when inferences are made for F0 and F1 in the same phase. Things work fine when AssertPipeable<F0, F1> is replaced with just F1 because we can then make independent inferences for F0 and F1 in different phases of the type inference process.

  4. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    BugA bug in TypeScript
    on Jul 1, 2019
  5. 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