You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Conditional result is incorrect when providing generics parameters to a type within a classΒ #49769
interfaceIIObj<GValue>{transform: (value: GValue)=>GValue;}exporttypeTEmptyArrayIfEqual<Gval1,Gval2>=Gval1extendsGval2 ? [] : IIObj<Gval1>;classTest<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
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.
Generics are kind of like, if you write a function:
functionfoo(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.
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?
Hmm, that's interesting. For the record, the concerns about never do still apply:
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 []exporttypeTEmptyArrayIfEqual<Gval1,Gval2>=Gval1extendsGval2 ? [] : {foo: number};// ...but this doesn'texporttypeTEmptyArrayIfEqual<Gval1,Gval2>=Gval1extendsGval2 ? [] : {foo: Gval1};
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
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.
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.
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.
Activity
MartinJohns commentedon Jul 3, 2022
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 typeGGenericis. Same as #48243.The type
GGenericcan still benever, in which case the typeTEmptyArrayIfEqual<GGeneric, GGeneric>resolves tonever, and[]is not assignable tonever.igor-quotient commentedon Jul 3, 2022
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 deferredIs there a way I can make the generic type arguments "bound"?
MartinJohns commentedon Jul 3, 2022
neverextendsnumber.neverextends every type.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 typenumber.This is a design limitation.
fatcerberus commentedon Jul 3, 2022
Generics are kind of like, if you write a function:
Inside the function, you only know
xwill be a number, you can't assume anything else about it besides what its type annotation requires.xis 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 typeGGenericcan 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.sharshuv-quotient commentedon Jul 4, 2022
I'm not sure I understand...
if I change the left side of the condition like so:
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 includeGval1?Why doesn't the explanation about the possibility of "never" hold in the first case?
fatcerberus commentedon Jul 4, 2022
Hmm, that's interesting. For the record, the concerns about
neverdo still apply:It seems that TS takes a shortcut on
T extends UwhereTandUare the same generic type and automatically takes the first branch, and what actually trips it up in this case isIIObj<Gval1>, which forces the whole thing to be deferred since conditional types distribute (i.e.CT<T | U> = CT<T> | CT<U>) andGval1could 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 forT extends T ? [] : {}because the types involved don't change regardless of how many members are inT({} | {} | {}is the same as{}, and likewise for[]).Evidence for the above:
fatcerberus commentedon Jul 4, 2022
Because TS has the above described shortcutting behavior, if you don't need the distributive behavior, this would be a workaround:
but n.b. it will behave differently from the original, distributive version:
RyanCavanaugh commentedon Jul 5, 2022
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
fatcerberus commentedon Jul 5, 2022
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?
RyanCavanaugh commentedon Jul 5, 2022
I mean writing a conditional type like
T extends U ? A : BwhereAandBare disjoint.sharshuv-quotient commentedon Jul 6, 2022
Ryan Cavanaugh (@RyanCavanaugh) Can you explain how the issue in the twitter post relates to the issue above?
RyanCavanaugh commentedon Jul 6, 2022
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 betweenstring | numberandstring, but if you're given a string at runtime there's no way to tell if it might have legally been a number.typescript-bot commentedon Jul 9, 2022
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.