Skip to content

Track recursive homomorphic mapped types by the symbol of their target #55638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23484,10 +23484,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// unique AST node.
return (type as TypeReference).node!;
}
if (type.symbol && !(getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol.flags & SymbolFlags.Class)) {
// We track all object types that have an associated symbol (representing the origin of the type), but
// exclude the static side of classes from this check since it shares its symbol with the instance side.
return type.symbol;
if (type.symbol) {
// We track object types that have a symbol by that symbol (representing the origin of the type).
if (getObjectFlags(type) & ObjectFlags.Mapped) {
// When a homomorphic mapped type is applied to a type with a symbol, we use the symbol of that
// type as the recursion identity. This is a better strategy than using the symbol of the mapped
// type, which doesn't work well for recursive mapped types.
type = getMappedTargetWithSymbol(type);
}
if (!(getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol.flags & SymbolFlags.Class)) {
// We exclude the static side of a class since it shares its symbol with the instance side.
return type.symbol;
}
}
if (isTupleType(type)) {
return type.target;
Expand All @@ -23511,6 +23519,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type;
}

function getMappedTargetWithSymbol(type: Type) {
let target = type;
while ((getObjectFlags(target) & ObjectFlags.InstantiatedMapped) === ObjectFlags.InstantiatedMapped && isMappedTypeWithKeyofConstraintDeclaration(target as MappedType)) {
target = getModifiersTypeFromMappedType(target as MappedType);
}
return target.symbol ? target : type;
}

function isPropertyIdenticalTo(sourceProp: Symbol, targetProp: Symbol): boolean {
return compareProperties(sourceProp, targetProp, compareTypesIdentical) !== Ternary.False;
}
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6204,6 +6204,8 @@ export const enum ObjectFlags {
RequiresWidening = ContainsWideningType | ContainsObjectOrArrayLiteral,
/** @internal */
PropagatingFlags = ContainsWideningType | ContainsObjectOrArrayLiteral | NonInferrableType,
/** @internal */
InstantiatedMapped = Mapped | Instantiated,
// Object flags that uniquely identify the kind of ObjectType
/** @internal */
ObjectTypeKindMask = ClassOrInterface | Reference | Tuple | Anonymous | Mapped | ReverseMapped | EvolvingArray,
Expand Down
63 changes: 63 additions & 0 deletions tests/baselines/reference/deeplyNestedMappedTypes.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
deeplyNestedMappedTypes.ts(9,7): error TS2322: Type 'Id<{ x: { y: { z: { a: { b: { c: number; }; }; }; }; }; }>' is not assignable to type 'Id<{ x: { y: { z: { a: { b: { c: string; }; }; }; }; }; }>'.
The types of 'x.y.z.a.b.c' are incompatible between these types.
Type 'number' is not assignable to type 'string'.
deeplyNestedMappedTypes.ts(17,7): error TS2322: Type 'Id2<{ x: { y: { z: { a: { b: { c: number; }; }; }; }; }; }>' is not assignable to type 'Id2<{ x: { y: { z: { a: { b: { c: string; }; }; }; }; }; }>'.
The types of 'x.y.z.a.b.c' are incompatible between these types.
Type 'number' is not assignable to type 'string'.


==== deeplyNestedMappedTypes.ts (2 errors) ====
// Simplified repro from #55535

type Id<T> = { [K in keyof T]: Id<T[K]> };

type Foo1 = Id<{ x: { y: { z: { a: { b: { c: number } } } } } }>;
type Foo2 = Id<{ x: { y: { z: { a: { b: { c: string } } } } } }>;

declare const foo1: Foo1;
const foo2: Foo2 = foo1; // Error expected
~~~~
!!! error TS2322: Type 'Id<{ x: { y: { z: { a: { b: { c: number; }; }; }; }; }; }>' is not assignable to type 'Id<{ x: { y: { z: { a: { b: { c: string; }; }; }; }; }; }>'.
!!! error TS2322: The types of 'x.y.z.a.b.c' are incompatible between these types.
!!! error TS2322: Type 'number' is not assignable to type 'string'.

type Id2<T> = { [K in keyof T]: Id2<Id2<T[K]>> };

type Foo3 = Id2<{ x: { y: { z: { a: { b: { c: number } } } } } }>;
type Foo4 = Id2<{ x: { y: { z: { a: { b: { c: string } } } } } }>;

declare const foo3: Foo3;
const foo4: Foo4 = foo3; // Error expected
~~~~
!!! error TS2322: Type 'Id2<{ x: { y: { z: { a: { b: { c: number; }; }; }; }; }; }>' is not assignable to type 'Id2<{ x: { y: { z: { a: { b: { c: string; }; }; }; }; }; }>'.
!!! error TS2322: The types of 'x.y.z.a.b.c' are incompatible between these types.
!!! error TS2322: Type 'number' is not assignable to type 'string'.

// Repro from issue linked in #55535

type RequiredDeep<T> = { [K in keyof T]-?: RequiredDeep<T[K]> };

type A = { a?: { b: { c: 1 | { d: 2000 } }}}
type B = { a?: { b: { c: { d: { e: { f: { g: 2 }}}}, x: 1000 }}}

type C = RequiredDeep<A>;
type D = RequiredDeep<B>;

type Test1 = [C, D] extends [D, C] ? true : false; // false
type Test2 = C extends D ? true : false; // false
type Test3 = D extends C ? true : false; // false

// Simplified repro from #54246

// Except for the final non-recursive Record<K, V>, object types produced by NestedRecord all have the same symbol
// and thus are considered deeply nested after three levels of nesting. Ideally we'd detect that recursion in this
// type always terminates, but we're unaware of a general algorithm that accomplishes this.

type NestedRecord<K extends string, V> = K extends `${infer K0}.${infer KR}` ? { [P in K0]: NestedRecord<KR, V> } : Record<K, V>;

type Bar1 = NestedRecord<"x.y.z.a.b.c", number>;
type Bar2 = NestedRecord<"x.y.z.a.b.c", string>;

declare const bar1: Bar1;
const bar2: Bar2 = bar1; // Error expected

177 changes: 177 additions & 0 deletions tests/baselines/reference/deeplyNestedMappedTypes.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//// [tests/cases/compiler/deeplyNestedMappedTypes.ts] ////

=== deeplyNestedMappedTypes.ts ===
// Simplified repro from #55535

type Id<T> = { [K in keyof T]: Id<T[K]> };
>Id : Symbol(Id, Decl(deeplyNestedMappedTypes.ts, 0, 0))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 2, 8))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 2, 16))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 2, 8))
>Id : Symbol(Id, Decl(deeplyNestedMappedTypes.ts, 0, 0))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 2, 8))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 2, 16))

type Foo1 = Id<{ x: { y: { z: { a: { b: { c: number } } } } } }>;
>Foo1 : Symbol(Foo1, Decl(deeplyNestedMappedTypes.ts, 2, 42))
>Id : Symbol(Id, Decl(deeplyNestedMappedTypes.ts, 0, 0))
>x : Symbol(x, Decl(deeplyNestedMappedTypes.ts, 4, 16))
>y : Symbol(y, Decl(deeplyNestedMappedTypes.ts, 4, 21))
>z : Symbol(z, Decl(deeplyNestedMappedTypes.ts, 4, 26))
>a : Symbol(a, Decl(deeplyNestedMappedTypes.ts, 4, 31))
>b : Symbol(b, Decl(deeplyNestedMappedTypes.ts, 4, 36))
>c : Symbol(c, Decl(deeplyNestedMappedTypes.ts, 4, 41))

type Foo2 = Id<{ x: { y: { z: { a: { b: { c: string } } } } } }>;
>Foo2 : Symbol(Foo2, Decl(deeplyNestedMappedTypes.ts, 4, 65))
>Id : Symbol(Id, Decl(deeplyNestedMappedTypes.ts, 0, 0))
>x : Symbol(x, Decl(deeplyNestedMappedTypes.ts, 5, 16))
>y : Symbol(y, Decl(deeplyNestedMappedTypes.ts, 5, 21))
>z : Symbol(z, Decl(deeplyNestedMappedTypes.ts, 5, 26))
>a : Symbol(a, Decl(deeplyNestedMappedTypes.ts, 5, 31))
>b : Symbol(b, Decl(deeplyNestedMappedTypes.ts, 5, 36))
>c : Symbol(c, Decl(deeplyNestedMappedTypes.ts, 5, 41))

declare const foo1: Foo1;
>foo1 : Symbol(foo1, Decl(deeplyNestedMappedTypes.ts, 7, 13))
>Foo1 : Symbol(Foo1, Decl(deeplyNestedMappedTypes.ts, 2, 42))

const foo2: Foo2 = foo1; // Error expected
>foo2 : Symbol(foo2, Decl(deeplyNestedMappedTypes.ts, 8, 5))
>Foo2 : Symbol(Foo2, Decl(deeplyNestedMappedTypes.ts, 4, 65))
>foo1 : Symbol(foo1, Decl(deeplyNestedMappedTypes.ts, 7, 13))

type Id2<T> = { [K in keyof T]: Id2<Id2<T[K]>> };
>Id2 : Symbol(Id2, Decl(deeplyNestedMappedTypes.ts, 8, 24))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 10, 9))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 10, 17))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 10, 9))
>Id2 : Symbol(Id2, Decl(deeplyNestedMappedTypes.ts, 8, 24))
>Id2 : Symbol(Id2, Decl(deeplyNestedMappedTypes.ts, 8, 24))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 10, 9))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 10, 17))

type Foo3 = Id2<{ x: { y: { z: { a: { b: { c: number } } } } } }>;
>Foo3 : Symbol(Foo3, Decl(deeplyNestedMappedTypes.ts, 10, 49))
>Id2 : Symbol(Id2, Decl(deeplyNestedMappedTypes.ts, 8, 24))
>x : Symbol(x, Decl(deeplyNestedMappedTypes.ts, 12, 17))
>y : Symbol(y, Decl(deeplyNestedMappedTypes.ts, 12, 22))
>z : Symbol(z, Decl(deeplyNestedMappedTypes.ts, 12, 27))
>a : Symbol(a, Decl(deeplyNestedMappedTypes.ts, 12, 32))
>b : Symbol(b, Decl(deeplyNestedMappedTypes.ts, 12, 37))
>c : Symbol(c, Decl(deeplyNestedMappedTypes.ts, 12, 42))

type Foo4 = Id2<{ x: { y: { z: { a: { b: { c: string } } } } } }>;
>Foo4 : Symbol(Foo4, Decl(deeplyNestedMappedTypes.ts, 12, 66))
>Id2 : Symbol(Id2, Decl(deeplyNestedMappedTypes.ts, 8, 24))
>x : Symbol(x, Decl(deeplyNestedMappedTypes.ts, 13, 17))
>y : Symbol(y, Decl(deeplyNestedMappedTypes.ts, 13, 22))
>z : Symbol(z, Decl(deeplyNestedMappedTypes.ts, 13, 27))
>a : Symbol(a, Decl(deeplyNestedMappedTypes.ts, 13, 32))
>b : Symbol(b, Decl(deeplyNestedMappedTypes.ts, 13, 37))
>c : Symbol(c, Decl(deeplyNestedMappedTypes.ts, 13, 42))

declare const foo3: Foo3;
>foo3 : Symbol(foo3, Decl(deeplyNestedMappedTypes.ts, 15, 13))
>Foo3 : Symbol(Foo3, Decl(deeplyNestedMappedTypes.ts, 10, 49))

const foo4: Foo4 = foo3; // Error expected
>foo4 : Symbol(foo4, Decl(deeplyNestedMappedTypes.ts, 16, 5))
>Foo4 : Symbol(Foo4, Decl(deeplyNestedMappedTypes.ts, 12, 66))
>foo3 : Symbol(foo3, Decl(deeplyNestedMappedTypes.ts, 15, 13))

// Repro from issue linked in #55535

type RequiredDeep<T> = { [K in keyof T]-?: RequiredDeep<T[K]> };
>RequiredDeep : Symbol(RequiredDeep, Decl(deeplyNestedMappedTypes.ts, 16, 24))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 20, 18))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 20, 26))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 20, 18))
>RequiredDeep : Symbol(RequiredDeep, Decl(deeplyNestedMappedTypes.ts, 16, 24))
>T : Symbol(T, Decl(deeplyNestedMappedTypes.ts, 20, 18))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 20, 26))

type A = { a?: { b: { c: 1 | { d: 2000 } }}}
>A : Symbol(A, Decl(deeplyNestedMappedTypes.ts, 20, 64))
>a : Symbol(a, Decl(deeplyNestedMappedTypes.ts, 22, 10))
>b : Symbol(b, Decl(deeplyNestedMappedTypes.ts, 22, 16))
>c : Symbol(c, Decl(deeplyNestedMappedTypes.ts, 22, 21))
>d : Symbol(d, Decl(deeplyNestedMappedTypes.ts, 22, 30))

type B = { a?: { b: { c: { d: { e: { f: { g: 2 }}}}, x: 1000 }}}
>B : Symbol(B, Decl(deeplyNestedMappedTypes.ts, 22, 44))
>a : Symbol(a, Decl(deeplyNestedMappedTypes.ts, 23, 10))
>b : Symbol(b, Decl(deeplyNestedMappedTypes.ts, 23, 16))
>c : Symbol(c, Decl(deeplyNestedMappedTypes.ts, 23, 21))
>d : Symbol(d, Decl(deeplyNestedMappedTypes.ts, 23, 26))
>e : Symbol(e, Decl(deeplyNestedMappedTypes.ts, 23, 31))
>f : Symbol(f, Decl(deeplyNestedMappedTypes.ts, 23, 36))
>g : Symbol(g, Decl(deeplyNestedMappedTypes.ts, 23, 41))
>x : Symbol(x, Decl(deeplyNestedMappedTypes.ts, 23, 52))

type C = RequiredDeep<A>;
>C : Symbol(C, Decl(deeplyNestedMappedTypes.ts, 23, 64))
>RequiredDeep : Symbol(RequiredDeep, Decl(deeplyNestedMappedTypes.ts, 16, 24))
>A : Symbol(A, Decl(deeplyNestedMappedTypes.ts, 20, 64))

type D = RequiredDeep<B>;
>D : Symbol(D, Decl(deeplyNestedMappedTypes.ts, 25, 25))
>RequiredDeep : Symbol(RequiredDeep, Decl(deeplyNestedMappedTypes.ts, 16, 24))
>B : Symbol(B, Decl(deeplyNestedMappedTypes.ts, 22, 44))

type Test1 = [C, D] extends [D, C] ? true : false; // false
>Test1 : Symbol(Test1, Decl(deeplyNestedMappedTypes.ts, 26, 25))
>C : Symbol(C, Decl(deeplyNestedMappedTypes.ts, 23, 64))
>D : Symbol(D, Decl(deeplyNestedMappedTypes.ts, 25, 25))
>D : Symbol(D, Decl(deeplyNestedMappedTypes.ts, 25, 25))
>C : Symbol(C, Decl(deeplyNestedMappedTypes.ts, 23, 64))

type Test2 = C extends D ? true : false; // false
>Test2 : Symbol(Test2, Decl(deeplyNestedMappedTypes.ts, 28, 50))
>C : Symbol(C, Decl(deeplyNestedMappedTypes.ts, 23, 64))
>D : Symbol(D, Decl(deeplyNestedMappedTypes.ts, 25, 25))

type Test3 = D extends C ? true : false; // false
>Test3 : Symbol(Test3, Decl(deeplyNestedMappedTypes.ts, 29, 40))
>D : Symbol(D, Decl(deeplyNestedMappedTypes.ts, 25, 25))
>C : Symbol(C, Decl(deeplyNestedMappedTypes.ts, 23, 64))

// Simplified repro from #54246

// Except for the final non-recursive Record<K, V>, object types produced by NestedRecord all have the same symbol
// and thus are considered deeply nested after three levels of nesting. Ideally we'd detect that recursion in this
// type always terminates, but we're unaware of a general algorithm that accomplishes this.

type NestedRecord<K extends string, V> = K extends `${infer K0}.${infer KR}` ? { [P in K0]: NestedRecord<KR, V> } : Record<K, V>;
>NestedRecord : Symbol(NestedRecord, Decl(deeplyNestedMappedTypes.ts, 30, 40))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 38, 18))
>V : Symbol(V, Decl(deeplyNestedMappedTypes.ts, 38, 35))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 38, 18))
>K0 : Symbol(K0, Decl(deeplyNestedMappedTypes.ts, 38, 59))
>KR : Symbol(KR, Decl(deeplyNestedMappedTypes.ts, 38, 71))
>P : Symbol(P, Decl(deeplyNestedMappedTypes.ts, 38, 82))
>K0 : Symbol(K0, Decl(deeplyNestedMappedTypes.ts, 38, 59))
>NestedRecord : Symbol(NestedRecord, Decl(deeplyNestedMappedTypes.ts, 30, 40))
>KR : Symbol(KR, Decl(deeplyNestedMappedTypes.ts, 38, 71))
>V : Symbol(V, Decl(deeplyNestedMappedTypes.ts, 38, 35))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>K : Symbol(K, Decl(deeplyNestedMappedTypes.ts, 38, 18))
>V : Symbol(V, Decl(deeplyNestedMappedTypes.ts, 38, 35))

type Bar1 = NestedRecord<"x.y.z.a.b.c", number>;
>Bar1 : Symbol(Bar1, Decl(deeplyNestedMappedTypes.ts, 38, 129))
>NestedRecord : Symbol(NestedRecord, Decl(deeplyNestedMappedTypes.ts, 30, 40))

type Bar2 = NestedRecord<"x.y.z.a.b.c", string>;
>Bar2 : Symbol(Bar2, Decl(deeplyNestedMappedTypes.ts, 40, 48))
>NestedRecord : Symbol(NestedRecord, Decl(deeplyNestedMappedTypes.ts, 30, 40))

declare const bar1: Bar1;
>bar1 : Symbol(bar1, Decl(deeplyNestedMappedTypes.ts, 43, 13))
>Bar1 : Symbol(Bar1, Decl(deeplyNestedMappedTypes.ts, 38, 129))

const bar2: Bar2 = bar1; // Error expected
>bar2 : Symbol(bar2, Decl(deeplyNestedMappedTypes.ts, 44, 5))
>Bar2 : Symbol(Bar2, Decl(deeplyNestedMappedTypes.ts, 40, 48))
>bar1 : Symbol(bar1, Decl(deeplyNestedMappedTypes.ts, 43, 13))

Loading