Skip to content

Contravariant inference breaks on generic conditional types. #56344

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
rzvc opened this issue Nov 8, 2023 · 7 comments
Closed

Contravariant inference breaks on generic conditional types. #56344

rzvc opened this issue Nov 8, 2023 · 7 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@rzvc
Copy link

rzvc commented Nov 8, 2023

🔎 Search Terms

mapped types templates contravariant inference

🕗 Version & Regression Information

  • This changed between versions 4.1.5 and 4.2.3.
  • This is the behavior in every version I tried.

⏯ Playground Link

https://tsplay.dev/NVrBBN

💻 Code

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

function test<T>(value: T)
{
    type I = UnionToIntersection<
          { a: number }
        | { b: number }
        | (T extends number ? { c: T } : { c: T })
    >;

    let i : I = null as any;

    i.a = 1;    // With c defined, i.a is available only up to version 4.1.5. Afterwards, it breaks.
    i.b = 1;    // Same for i.b.
    i.c = value;
}

🙁 Actual behavior

The non-generic dependent properties were not available.

🙂 Expected behavior

At least the non-generic dependent properties to be available. Ideally, c should have been available too and value should have been assignable to it, but that's less "expected" than the first part.

Additional information about the issue

It completely breaks more complex mapped types that used to work up to 4.1.5. The conditional generic type was still not available, but it was better than nothing.

If the previous behaviour cannot be restored, perhaps there's a way to detect that you're dealing with a generic type which will be deferred, and allow the programmer to decide what to do with it?

@jcalz
Copy link
Contributor

jcalz commented Nov 8, 2023

You might consider coming up with an example that doesn't depend on UnionToIntersection since that tends to get issues closed: #43077 (comment); #56185; see #29594 (comment) also. Can you reproduce the basic issue with a less problematic use of contravariant inference?

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Nov 8, 2023
@rzvc
Copy link
Author

rzvc commented Nov 8, 2023

@jcalz, not sure if this is what you meant by less problematic, but would this do it?

function test<T>(value: T)
{
    type I = (
          ((arg: { a: number }) => void)
        | ((arg: { b: number }) => void)
        | (T extends number ? (arg: { c: T }) => void : (arg: { c: T }) => void)
    ) extends ((arg: infer I) => void) ? I : never;

    let i : I = null as any;

    i.a = 1;    // With c defined, i.a is available only up to version 4.1.5. Afterwards, it breaks.
    i.b = 1;    // Same for i.b.
    i.c = value;
}

https://tsplay.dev/w2QOrW

@jcalz
Copy link
Contributor

jcalz commented Nov 8, 2023

I imagine the type should have some obvious real-world utility for a use case the TS team is interested in supporting, but I can't say what that is on first glance. 🤷‍♂️

@rzvc
Copy link
Author

rzvc commented Nov 8, 2023

@jcalz, well, that kinda pulls UnionToIntersection back into the conversation, because it's useful when you have to break a type apart with something like this, and then put the result back together:

type Remap<T> = {
    [K in keyof T]: T[K] extends number
        ? { [P in K]: T[K] }
        : { [P in K]: T[K] }
}[keyof T];

@fatcerberus
Copy link

I’m confused why you’re writing T extends U ? V : V (presumably this is just to get the distributive behavior?) instead of something like T extends unknown ? V : never

Although in the case of Remap there’s seemingly no reason to write that conditional type at all since T[K] extends … is not distributive…

@rzvc
Copy link
Author

rzvc commented Nov 9, 2023

@fatcerberus, Remap is just an example, not a real world thing. I wrote it to demonstrate the type of mapping with conditional types, that would require contravariant inference to work when one of the properties of the type being mapped has a generic type.

In its extends clause, I'm not looking for enabling the distributive behaviour, but to show that the types I'm having problems with are doing some extends logic in the remapping process, which is a necessary ingredient to break inference.

@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 Nov 12, 2023
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