Skip to content

Commit

Permalink
Track recursive homomorphic mapped types by the symbol of their target (
Browse files Browse the repository at this point in the history
  • Loading branch information
ahejlsberg authored Sep 11, 2023
1 parent eb2d1f9 commit 4f899a1
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 4 deletions.
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

0 comments on commit 4f899a1

Please sign in to comment.