Repository navigation
3.5.1 regression when using this with an index signature of a generic union type #31731
Description
Activity
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.- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Jun 3, 2019 justingrant commented
on Jun 3, 2019 ContributorAuthorMore actionsAnders 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?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.Reacted by Justin Grantjustingrant commented
on Jun 4, 2019 ContributorAuthorMore actionsMakes sense. Thanks so much Anders Hejlsberg (@ahejlsberg) for the investigation and clear explanation of what's going on.
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):export type Runtype<T = unknown> = { T: T; // Witness guard(value: unknown): value is T; } export type Record<T extends { [key: string]: Runtype }> = { [P in keyof T]: T[P]['T'] } export declare const Record: <T extends { [key: string]: Runtype }>(def: T) => Runtype<Record<T>>; export type Union<T extends Runtype[]> = T[number]['T']; export declare const Union: <T extends Runtype[]>(...types: T) => Runtype<Union<T>>; type UnionToIntersection<U> = (U extends U ? (x: U) => void : never) extends (x: infer I) => void ? I : never export type Intersect<T extends Runtype[]> = UnionToIntersection<T[number]['T']>; export declare const Intersect: <T extends Runtype[]>(...types: T) => Runtype<Intersect<T>>; export declare const String: Runtype<string>; export declare const Number: Runtype<number>; export declare const Literal: <T extends string | number | boolean>(value: T) => Runtype<T>; const Boolean = Union(Literal(true), Literal(false)); // { s: string, b: boolean } & { n: number } | string const MyType = Union(Intersect(Record({ s: String, b: Boolean }), Record({ n: Number })), String); function foo(x: unknown) { if (MyType.guard(x)) { if (typeof x === "string") { return x; } else { if (x.b) { return "true"; } if (x.n > 10) { return "" + x.n; } return x.s; } } return "Error"; }
Reacted by Tom Crockett- locked as resolved and limited conversation to collaborators
on Oct 21, 2025
TS 3.5 broke the runtypes library which uses
thisas 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 bothAandBare generic types with avalueproperty) compiled OK, but in 3.5.1 and 3.6.0-dev.20190602 it triggers a "not assignable to" error whenthisis 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
thisin #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
Expected behavior:
No compiler errors (like in 3.4.5)
Actual behavior:
Playground Link: https://www.typescriptlang.org/play/#src=%2F%2F%20this%20declaration%20fails%20in%203.5.1%20(and%203.6.0-dev.20190602)%2C%20but%20OK%20in%203.4.5%20%0D%0Ainterface%20R%3CA%20%3D%20unknown%3E%20%7B%0D%0A%20%20value%3A%20A%3B%0D%0A%20%20Or%3CB%20extends%20R%3E(b%3A%20B)%3A%20Union%3Cthis%2C%20B%3E%3B%20%2F%2F%20compiler%20error%20on%20%60this%60%0D%0A%7D%0D%0Ainterface%20Union%3CA%20extends%20R%2C%20B%20extends%20R%3E%20extends%20R%3C(A%20%7C%20B)%5B'value'%5D%3E%20%7B%7D%0D%0A%0D%0A%2F%2F%20this%20declaration%20works%20OK%20in%203.4.5%2C%203.5.1%2C%20and%203.6.0-dev.20190602%0D%0A%2F%2F%20The%20only%20difference%20is%20using%20%60A%5B'value'%5D%20%7C%20B%5B'value'%5D%60%20instead%20of%20%60(A%20%7C%20B)%5B'value'%5D%60%0D%0Ainterface%20R2%3CA%20%3D%20unknown%3E%20%7B%0D%0A%20%20value%3A%20A%3B%0D%0A%20%20Or%3CB%20extends%20R2%3E(b%3A%20B)%3A%20Union2%3Cthis%2C%20B%3E%3B%0D%0A%7D%0D%0Ainterface%20Union2%3CA%20extends%20R2%2C%20B%20extends%20R2%3E%20extends%20R2%3CA%5B'value'%5D%20%7C%20B%5B'value'%5D%3E%20%7B%7D
Related Issues: #31691 #31454 #31704 #31439 #30769