GeezerVison is showing github.com — 3.5.1 regression when using `this` with an index signature of a generic union type · Issue #31731 · microsoft/TypeScript
3.5.1 regression when using this with an index signature of a generic union type #31731DescriptionTS 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: [email protected]
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)
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 Activityahejlsberg commented on Jun 3, 2019 This looks like an effect of #30769 which is a known breaking change. It used to be that (A | B)['value'] and A['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 commented on Jun 3, 2019 ContributorAuthor More actions 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 of A and B are now valid in this context? Or is the problem that using this makes it a recursive type and the compiler can't validate the type as OK before it hits a recursion limit and gives up? ahejlsberg commented on Jun 4, 2019 Justin Grant (@justingrant) Yes, the reason for the difference is that (A | B)['value'] now simplifies to A['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 Union interface is written as R<A['value'] | B['value']> we end up with a circularity as we're measuring the variance of R<A> and Union<A, B>. We therefore default both of those types to have co-variant type parameters. That in turn means that R<A> is related to R<unknown>.
When the base type is written as R<(A | B)['value']>, we'd previously simplify to the type above, but we now simplify to R<A['value'] & B['value']>. This type is restrictive enough that we don't observe a circularity, and we therefore measure R<A> and Union<A, B> to have invariant type parameters. That in turn means R<A> and Union<A, B> are only compatible with identical instantiations of themselves. justingrant commented on Jun 4, 2019 ContributorAuthor More actions ahejlsberg commented on Jun 4, 2019 Justin Grant (@justingrant) You're welcome. BTW, as I was looking into this I noticed the very large sets of overloaded Union and Intersection types 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";
}locked as resolved and limited conversation to collaborators on Oct 21, 2025
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|