Skip to content

Inferred type changes from 5.5 to 5.6 #60077

Description

@danvk

🔎 Search Terms

  • variance

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.2#code/CYUwxgNghgTiAEYD2A7AzgF3gMwFzwB4AFAPgAoAoeHFfMgBxiXrXyIEp4BeE+ANyQBLYABoq8QSkEYA-GzGce8IgG4KFSRhAxsUMAiJMW8AN7i4AR3yYYkgOZrqzWdYy2UDigF91ydFkZmNG4cSmoGNiM0RV4AclixahN4S3wAIjT4LwU1CkCWADpnCgB6EvgAURgmGAkUeAAVAGV4AFYCgDYReBQkLEl4EFgIQW1+bTRBVDQKIA

💻 Code

declare const f: <P>(
  fn: (props: P) => void,
  init?: P,
) => P;

interface Props {
  req: string;
  opt?: string;
}

const props = f(
  (p: Props) => '',
  { req: "" },
);

props.opt
// Error in TS 5.6, not in earlier versions

🙁 Actual behavior

P is inferred as {req: string}, so accessing the optional property causes a TypeScript error.

🙂 Expected behavior

P should be inferred as Props, as it is in TS 5.5 (and earlier). If you drop the init parameter from the call to f, then P is also inferred to be Props in TS 5.6.

Additional information about the issue

This seems related to #59764 but I'm not sure it's the same, so I figured I'd file an issue. I tried the same code on the playground for #59709 and it does not fix this issue.

cc Mateusz Burzyński (@Andarist) for whether TS 5.6 is right and I'm wrong 😄

Activity

  1. Andarist commented on Sep 26, 2024

    @Andarist
    Contributor

    This seems related to #59764 but I'm not sure it's the same

    I think this boils down to the same issue and I would refer to Ryan's comments there: #59764 (comment) and #59764 (comment)

    Those are just heuristics and there are no perfect answers to which type should be preferred. Some cases perform better with one candidate picked and some cases perform better with the other one picked.

    I think that especially the case presented by Ryan in the first linked comment sways this way more in favor of the covariant candidate. It's way more likely that the object's shape available at runtime will have the properties coming from a covariant argument position than those coming from a contravariant parameter.

  2. RyanCavanaugh commented on Sep 26, 2024

    @RyanCavanaugh
    Member

    I agree with Mateusz Burzyński (@Andarist) that I was correct in those comments 🙂

  3. danvk commented on Sep 26, 2024

    @danvk
    ContributorAuthor

    OK, I can see I'm outgunned here. 🙂

    FWIW, I do think it's a bit surprising that this wasn't mentioned in the release notes given that it can cause confusing changes when you upgrade — I only found the PR via every-ts bisect.

  4. danvk commented on Sep 27, 2024

    @danvk
    ContributorAuthor

    Though I guess looking at #59764 (comment) a little more closely, one difference is that in that case, the widened type of defaultT isn't assignable to Foo (at least with excess property checking). Whereas in my example the type of init is assignable to Props, so that seems like a more natural type to infer.

    I can see that there's no perfect answer in Ryan's case, but it seems there is here.

  5. Andarist commented on Sep 27, 2024

    @Andarist
    Contributor

    That is a difference that I implicitly talked about here. Honestly, I don't know how to think about this in the TS's structural type system since there are tradeoffs to both approaches

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions