Skip to content

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

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
AnyhowStep opened this issue Jun 7, 2019 · 3 comments
Closed
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@AnyhowStep
Copy link
Contributor

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.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Jun 7, 2019

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

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Jun 25, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.6.0 milestone Jun 25, 2019
@RyanCavanaugh
Copy link
Member

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

@ahejlsberg
Copy link
Member

ahejlsberg commented Jul 1, 2019

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.

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Bug A bug in TypeScript labels Jul 1, 2019
@ahejlsberg ahejlsberg removed this from the TypeScript 3.6.0 milestone Jul 1, 2019
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