Skip to content

Conditional result is incorrect when providing generics parameters to a type within a classΒ #49769

Closed

Description

Bug Report

πŸ”Ž Search Terms

  • Conditional
  • Generics

πŸ•— Version & Regression Information

In 4.7.4

  • This changed between versions 4.4.4 and 4.7.4

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

interface IIObj<GValue> {
  transform: (value: GValue) => GValue;
}

export type TEmptyArrayIfEqual<Gval1, Gval2> = Gval1 extends Gval2 ? [] : IIObj<Gval1>;


class Test<GGeneric> {
  prop1: TEmptyArrayIfEqual<GGeneric, GGeneric> = []; // Type '[]' is not assignable to type 'TEmptyArrayIfEqual<GGeneric, GGeneric>'.(2322)
  prop2: TEmptyArrayIfEqual<number, number> = []; // works
}

πŸ™ Actual behavior

prop1 is not assignable to []

πŸ™‚ Expected behavior

prop1 should be assignable to empty array since same type(GGeneric )is provided as GVal1 & Gval2 params

Activity

  1. MartinJohns commented on Jul 3, 2022

    @MartinJohns
    Contributor

    Resolving of conditional types involving unbound generic type arguments is deferred. At that point the compiler doesn't know what TEmptyArrayIfEqual<GGeneric, GGeneric> resolves to, because the compiler doesn't know what type GGeneric is. Same as #48243.

    The type GGeneric can still be never, in which case the type TEmptyArrayIfEqual<GGeneric, GGeneric> resolves to never, and [] is not assignable to never.

  2. igor-quotient commented on Jul 3, 2022

    @igor-quotient
    Author

    Martin Johns (@MartinJohns) Thanks for the answer! So if I'll strict GGeneric(link) it should solve the 2nd case you have mentioned, right?

    Resolving of conditional types involving unbound generic type arguments is deferred
    Is there a way I can make the generic type arguments "bound"?

  3. MartinJohns commented on Jul 3, 2022

    @MartinJohns
    Contributor

    Thanks for the answer! So if I'll strict GGeneric(link) it should solve the 2nd case you have mentioned, right?

    never extends number. never extends every type.

    Resolving of conditional types involving unbound generic type arguments is deferred
    Is there a way I can make the generic type arguments "bound"?

    It is bound as soon as the compiler has an actual type for it. For example new Test<number>, now the type argument is bound to the type number.

    This is a design limitation.

  4. fatcerberus commented on Jul 3, 2022

    @fatcerberus

    Generics are kind of like, if you write a function:

    function foo(x: number) {
        console.log(x);
    }

    Inside the function, you only know x will be a number, you can't assume anything else about it besides what its type annotation requires. x is basically in superposition. So you're forced to write a function that works predictably for all possible numbers that the caller might use, lest you end up with weird bugs later.

    With generics, it's like that, but with types, which puts you on a pretty tight leash because the compiler actually cares about types in a way it doesn't about individual values. In class Test<GGeneric> the code inside the class must work for any type GGeneric can be, which can be literally any type at all, and the compiler actually tries to enforce this. That's limiting enough, but in your case you also have a conditional type involved, and so long as you're in a context where the input to the conditional type isn't known, TS treats it as essentially opaque - it's only resolved to a usable type once TS has a concrete input type to work with.

  5. sharshuv-quotient commented on Jul 4, 2022

    @sharshuv-quotient

    I'm not sure I understand...
    if I change the left side of the condition like so:

    type TEmptyArrayIfEqual<Gval1, Gval2> = Gval1 extends Gval2 ? [] : {};

    It works. So typescripts correctly understand that the left side of the condition ([]) is that type it should resolve to. Why does it matter that I change the other type to include Gval1 ?
    Why doesn't the explanation about the possibility of "never" hold in the first case?

  6. fatcerberus commented on Jul 4, 2022

    @fatcerberus

    Hmm, that's interesting. For the record, the concerns about never do still apply:

    image

    It seems that TS takes a shortcut on T extends U where T and U are the same generic type and automatically takes the first branch, and what actually trips it up in this case is IIObj<Gval1>, which forces the whole thing to be deferred since conditional types distribute (i.e. CT<T | U> = CT<T> | CT<U>) and Gval1 could be a union.

    Note that last part: "could be a union". That's the root of the issue AFAICT; TS can't take the aforementioned shortcut in the OP because it sees that distribution may produce an unknown number of instantiations of IIObj<Gval1>. This doesn't apply for T extends T ? [] : {} because the types involved don't change regardless of how many members are in T ({} | {} | {} is the same as {}, and likewise for []).

    Evidence for the above:

    // this works, and allows the assignment from []
    export type TEmptyArrayIfEqual<Gval1, Gval2> = Gval1 extends Gval2 ? [] : { foo: number };
    
    // ...but this doesn't
    export type TEmptyArrayIfEqual<Gval1, Gval2> = Gval1 extends Gval2 ? [] : { foo: Gval1 };
  7. fatcerberus commented on Jul 4, 2022

    @fatcerberus

    Because TS has the above described shortcutting behavior, if you don't need the distributive behavior, this would be a workaround:

    export type TEmptyArrayIfEqualND<Gval1, Gval2> = [Gval1] extends [Gval2] ? [] : IIObj<Gval1>;

    but n.b. it will behave differently from the original, distributive version:

    type T = TEmptyArrayIfEqual<string | number, string>;  // IIObj<string> | IIObj<number>
    type U = TEmptyArrayIfEqualND<string | number, string>;  // IIObj<string | number>
  8. RyanCavanaugh commented on Jul 5, 2022

    @RyanCavanaugh
    Member

    I think if we had been smarter, conditional types would have had restrictions on what kind of things you could write, with special syntax to opt into nonlinear behavior. See also https://twitter.com/SeaRyanC/status/1369397210094448643

  9. fatcerberus commented on Jul 5, 2022

    @fatcerberus

    If by "nonlinear behavior" you're referring to distributivity then yes, I've always thought it was... less than ideal that whether a conditional type was distributive was based on semantics (whether the input is a naked type parameter) rather than purely syntactic. It's one of my biggest pet peeves because almost every TS beginner seems to get tripped up by it at some point.

    Ryan Cavanaugh (@RyanCavanaugh) Was my analysis of the situation above accurate?

  10. RyanCavanaugh commented on Jul 5, 2022

    @RyanCavanaugh
    Member

    I mean writing a conditional type like T extends U ? A : B where A and B are disjoint.

  11. sharshuv-quotient commented on Jul 6, 2022

    @sharshuv-quotient

    Ryan Cavanaugh (@RyanCavanaugh) Can you explain how the issue in the twitter post relates to the issue above?

  12. RyanCavanaugh commented on Jul 6, 2022

    @RyanCavanaugh
    Member

    The type Gval1 extends Gval2 ? [] : IIObj<Gval1>; is suspect because it can't correspond to a real runtime behavior, since this type claims to e.g. make a distinction between string | number and string, but if you're given a string at runtime there's no way to tell if it might have legally been a number.

  13. typescript-bot commented on Jul 9, 2022

    @typescript-bot
    Contributor

    This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow or the TypeScript Discord community.

  14. 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