Repository navigation
3.5.1 regression when using this with an index signature of a generic union type #31731
Description
TS 3.5 broke the runtypes library which uses this as as a type parameter to a generic method of a generic interface. After simplifying the break into a 4 lines of TS declarations, it looks like the problem is caused by TS failing to correctly resolve types using the index signature of a union type where both sides of the union share the same property name but different generic property types. In 3.4, (A | B)['value'] (where both A and B are generic types with a value property) compiled OK, but in 3.5.1 and 3.6.0-dev.20190602 it triggers a "not assignable to" error when this is used as one of the type parameters.
FWIW, a slightly different syntax A['value'] | B['value'] works OK in both 3.4 and 3.5.
I see that Anders Hejlsberg (@ahejlsberg) has been working on assignment using this in #31704 and #31454, so perhaps this issue here is a case that slipped through those PRs?
TypeScript Version: typescript@3.6.0-dev.20190602
Search Terms: this interface typescript recursion recursive union index signature
Code
// this declaration fails in 3.5.1 (and 3.6.0-dev.20190602), but OK in 3.4.5
interface R<A = unknown> {
value: A;
Or<B extends R>(b: B): Union<this, B>; // compiler error on `this`
}
interface Union<A extends R, B extends R> extends R<(A | B)['value']> {}
// this declaration works OK in 3.4.5, 3.5.1, and 3.6.0-dev.20190602
// The only difference is using `A['value'] | B['value']` instead of `(A | B)['value']`
interface R2<A = unknown> {
value: A;
Or<B extends R2>(b: B): Union2<this, B>;
}
interface Union2<A extends R2, B extends R2> extends R2<A['value'] | B['value']> {}Expected behavior:
No compiler errors (like in 3.4.5)
Actual behavior:
Type 'this' does not satisfy the constraint 'R<unknown>'.
Type 'R<A>' is not assignable to type 'R<unknown>'.
Types of property 'Or' are incompatible.
Type '<B extends R<unknown>>(B: B) => Union<this, B>' is not assignable to type '<B extends R<unknown>>(B: B) => Union<R<unknown>, B>'.
Type 'Union<this, B>' is not assignable to type 'Union<R<unknown>, B>'.
Types of property 'Or' are incompatible.
Type '<B extends R<unknown>>(B: B) => Union<Union<this, B>, B>' is not assignable to type '<B extends R<unknown>>(B: B) => Union<Union<R<unknown>, B>, B>'.
Type 'Union<Union<this, B>, any>' is not assignable to type 'Union<Union<R<unknown>, B>, any>'.
Types of property 'Or' are incompatible.
Type '<B extends R<unknown>>(B: B) => Union<Union<Union<this, B>, any>, B>' is not assignable to type '<B extends R<unknown>>(B: B) => Union<Union<Union<R<unknown>, B>, any>, B>'.
Type 'Union<Union<Union<this, B>, any>, any>' is not assignable to type 'Union<Union<Union<R<unknown>, B>, any>, any>'.
Type 'Union<Union<R<unknown>, B>, any>' is not assignable to type 'Union<Union<this, B>, any>'.
Type 'Union<R<unknown>, B>' is not assignable to type 'Union<this, B>'.ts(2344)
Activity
ahejlsberg commentedon Jun 3, 2019
This looks like an effect of #30769 which is a known breaking change. It used to be that
(A | B)['value']andA['value'] | B['value']would behave the same on the target side of the assignable type relation, but(A | B)['value']is now subject to stricter rules.justingrant commentedon Jun 3, 2019
Anders Hejlsberg (@ahejlsberg) - is there a good heuristic to understand the new "stricter rules" for
(A | B)['value']? I read #30769 pretty carefully but I'm having trouble translating that spec into how it applies to this case. Do the new rules effectively treat it like(A & B)['value']because only the intersection ofAandBare now valid in this context? Or is the problem that usingthismakes it a recursive type and the compiler can't validate the type as OK before it hits a recursion limit and gives up?ahejlsberg commentedon Jun 4, 2019
Justin Grant (@justingrant) Yes, the reason for the difference is that
(A | B)['value']now simplifies toA['value'] & B['value']in a target position, and that is a more restrictive type.I did a bit of debugging to see what exactly is going on in the example.
When the base type of the
Unioninterface is written asR<A['value'] | B['value']>we end up with a circularity as we're measuring the variance ofR<A>andUnion<A, B>. We therefore default both of those types to have co-variant type parameters. That in turn means thatR<A>is related toR<unknown>.When the base type is written as
R<(A | B)['value']>, we'd previously simplify to the type above, but we now simplify toR<A['value'] & B['value']>. This type is restrictive enough that we don't observe a circularity, and we therefore measureR<A>andUnion<A, B>to have invariant type parameters. That in turn meansR<A>andUnion<A, B>are only compatible with identical instantiations of themselves.justingrant commentedon Jun 4, 2019
Makes sense. Thanks so much Anders Hejlsberg (@ahejlsberg) for the investigation and clear explanation of what's going on.
ahejlsberg commentedon Jun 4, 2019
Justin Grant (@justingrant) You're welcome. BTW, as I was looking into this I noticed the very large sets of overloaded
UnionandIntersectiontypes in the runtypes library. I think you can simplify those dramatically using some of our newer features like variadic functions (#24897). Here's a sketch of what I have in mind (mostly just the types, implementations left as an exercise for the reader):