Skip to content

5.6 regression: Inference related regression: Wider covariant inference gets picked over more narrow contravariant one #59764

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
trevorade opened this issue Aug 26, 2024 · 13 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@trevorade
Copy link

trevorade commented Aug 26, 2024

πŸ”Ž Search Terms

Inference

πŸ•— Version & Regression Information

  • This changed between versions 5.5.4 and 5.6.0-beta (still happening in 5.6.1-rc)

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.1-rc#code/JYOwLgpgTgZghgYwgAgGIHt3IN4ChnJgCeADhAFzIDkMmVA3PsuiWMOiHADYAKULAfkoAjTFwhwQjAL65cxMsgDCHAM4BXALbQAPEwD0+5KGbqwyQ8gCqIBOk3bwoAOanzYABbBVyLqBQARDDAAB4QqgGEHijeGhAAdEwAKrgAfMgAvMgAFHBQzpRJAJSZ6QBu6MAAJoy4VRAIXHkoMOq2bBzIqvYQqG0IOikEECGQIFU+GFiWKg4QTiCu6GZR3r7+yEGh4ZGeMapxianZTHYgGtpQlCrnWrpJqQA0TPXw6lxgSYXPRYW19Y1msgzqpzLR0DcLtBrmo7lAdFNUvRkHIQeYoOF3uYst1tH1bNlwZC4Y8cAoKNRwVRpEVGJYCMgAHoCXBAA

πŸ’» Code

interface Foo {
  type: 'foo';
  optionalProp?: boolean;
}

type Consumer<
  // in out // Uncommenting out this line "fixes" the issue.
  T
> = (arg: T) => void;

declare function someFunc<T
  extends Foo // Commenting out this line "fixes" the issue.
>(
  consumer: Consumer<T>,
  defaultT: T,
): T;

declare const fooConsumer: Consumer<Foo>; 

const result = someFunc(fooConsumer, {type: 'foo'});
//    ^?

πŸ™ Actual behavior

result is of type { type: "foo"; }

πŸ™‚ Expected behavior

result is of type Foo as it was in previous releases

Additional information about the issue

See the comments in the example code for two tweaks to make for the code to work as expected. I get adding in out changing things. I'm less clear on why removing the extends should make any difference.

(Google note: relevant source location http://shortn/_eCS9djFiAR)

@trevorade
Copy link
Author

To be clear, this is a real builder error observed in Google that has been boiled down to a minimal reproduction with changed names.

@trevorade
Copy link
Author

Confirmed that #59709 does not resolve this issue.

@RyanCavanaugh
Copy link
Member

This seems correct? Let's say you wrote this:

interface Foo {
  type: 'foo';
  optionalProp?: boolean;
}

type Consumer<T> = (arg: T) => void;

declare function someFunc<T extends Foo>(consumer: Consumer<T>, defaultT: T): T;

declare const fooConsumer: Consumer<Foo>;

const result = someFunc(fooConsumer, { type: 'foo', extra: "bar" });
result.extra; // should be OK!

@trevorade
Copy link
Author

trevorade commented Aug 26, 2024

So I have at least three more classes of similar regressions to this we're seeing in Google that I need to investigate and then create simplified repros.

My Consumer name was bad. Here's the real code with names changed to make it a little less clear on what you might infer to be the correct behavior:

function functionName<C extends SomeUnionOfTypes>(
  arg1: Map<SomeType, AnotherType<YetAnotherType<C>>>,
  arg2: C,
  arg3: SomeType,
  arg4: unknown,
): C

Looking at this, it's less clear if we should prefer the literal type of C as extracted from arg1 or arg2.

The behavior has changed though which is causing new regressions.

@trevorade
Copy link
Author

I was about to log a bug for this last largest class but, thinking about it more, I believe it boils down to this bug as well.

Here's the example:

declare class TestEnvironment<Args> {
    constructor(template?: (args: Args) => Element, defaultTemplateData?: Args);
    render(data?: Args): void;
}

declare function myElRender(data: MyRenderParams): Element;

type MyRenderParams = {
    state: State,
    optBool?: boolean,
};

enum State {
    OPEN,
    CLOSE,
}


const env = new TestEnvironment(myElRender, {state: State.OPEN});

function doTheRender({
  state = State.OPEN,
  optBool = false,
}: {state?: State, optBool?: boolean} = {}) {
  env.render({state, optBool});
}

Or, as a TS Playground:
https://www.typescriptlang.org/play/?ts=5.6.1-rc#code/CYUwxgNghgTiAEkoGdnwComQFwKIDsA3ASxgHt8BbEfbAHgEEYBzZAPngG8AoePxCjhgBXMNjIwAFNhCUADtBkB+AFzxJsVmqasAlPAC8HXBFk1sAGnigAZlGERsmeYpAARKNiir4O5LoBuXn44fFApYE9vbRZ-NUIyYmAggF9ublAkOHgbYXwxYgp4SgBPEwAlGnDJSK81AFkSyrCQGAAFWChKOPgTM1og7mwSuQRG5vCOmC60Ay5gvhxPEDUAZS8ZCwX4MjlsACEyMggfACMj0yh8LZTBmmFKeHXl+f54AHk23AA5LbeAYQAMu9VrgbuluGBBNh4DRCIZ4PgQAB3DBYPBEUgUai0SSlCpVVpWThLGRrDYgAB0nx+KUC6Vy+WwhXw1jI6AAFiAJq1JDxFhSEc8ZNSvr9grsDhcEXYIMgQDc1CSKT5hQqdntDsczhcQFcUgjOHTXrCiJTQtVlcsrJKtRA6aluEA

TestEnvironment is inferring the type of Args as {state: State} instead of MyRenderParams as it previously did.


From the sample runs I performed, I detected 26 different tests failing this same way.

This particular pattern impacts thousands of our tests in the Google codebase so is more important.

If you feel this is significantly different, I can log a separate issue for this regression.

@RyanCavanaugh
Copy link
Member

TestEnvironment as written just needs some help if the intended contract is that the template argument is the sole determinant of Args. This inference in both 5.5 and 5.6 doesn't match what you want to happen in cases where you pass optBool to defaultTemplateData

declare class TestEnvironment<Args> {
    constructor(template?: (args: Args) => Element, defaultTemplateData?: Args);
    render(data?: Args): void;
}

declare function myElRender(data: MyRenderParams): Element;

type MyRenderParams = {
    state: State,
    optBool?: boolean,
};

enum State {
    OPEN,
    CLOSE,
}

function f1() {
  const env = new TestEnvironment(myElRender, {state: State.OPEN});
  // OK
  env.render({ state: State.OPEN });
}

function f2() {
  const env = new TestEnvironment(myElRender, {state: State.OPEN, optBool: true});
  // optBool is required now??
  env.render({ state: State.OPEN });
}

This is a very good situation for using NoInfer, which fixes it in both cases and clarifies which argument the inference is supposed to happen from

    , defaultTemplateData?: NoInfer<Args>

@trevorade
Copy link
Author

I understand that I can rewrite code to make it compile with the new inference changes. I'm not sure that we should have to though.

My concern is that large swaths of pre-existing TypeScript is just going to stop compiling correctly around the world in this version. If that's okay, then okay.

For this particular case on our end, it's a little tricky because the TestEnvironment class is written in JavaScript, not TypeScript. I need to test how much I can influence the generated type declarations in this case.

@trevorade trevorade changed the title Inference related regression: should prefer named type rather than inferred generic type 5.6 regression: Inference related regression: should prefer named type rather than inferred generic type Aug 27, 2024
@Andarist
Copy link
Contributor

Note that the title of this issue is a little bit misguided. Both of those types are inferred here internally. The fact that one of them was "named" was always irrelevant. It's all about the algorithm that chooses the covariant vs contravariant inference candidates to be selected as the final inferred type.

@trevorade
Copy link
Author

Note that the title of this issue is a little bit misguided. Both of those types are inferred here internally. The fact that one of them was "named" was always irrelevant. It's all about the algorithm that chooses the covariant vs contravariant inference candidates to be selected as the final inferred type.

I'm happy to change the title. Can you suggest a more accurate one?

@Andarist
Copy link
Contributor

Wider covariant inference gets picked over more narrow contravariant one

@RyanCavanaugh
Copy link
Member

My concern is that large swaths of pre-existing TypeScript is just going to stop compiling correctly around the world in this version. If that's okay, then okay.

The available data we have is that this isn't going to be a disruptive change. The expected number of inference changes between versions isn't zero. The behavior change here is basically a "bug fix" in other contexts and it's expected to see similar-looking cases have similar-looking changes.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Aug 27, 2024
@trevorade trevorade changed the title 5.6 regression: Inference related regression: should prefer named type rather than inferred generic type 5.6 regression: Inference related regression: Wider covariant inference gets picked over more narrow contravariant one Aug 27, 2024
@trevorade
Copy link
Author

trevorade commented Aug 27, 2024

The available data we have is that this isn't going to be a disruptive change. The expected number of inference changes between versions isn't zero. The behavior change here is basically a "bug fix" in other contexts and it's expected to see similar-looking cases have similar-looking changes.

OK. That's good to hear. I'm familiar with the tools to resolve these issues on our end (NoInfer + multiple template params where one extends the other) so I can make updates in our code.

Thanks for being patient with me as I log these regressions. Still getting a feel for this process :)

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Aug 30, 2024
@github-staff github-staff deleted a comment Mar 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

5 participants
@RyanCavanaugh @Andarist @trevorade @typescript-bot and others