Skip to content

Commit b76b4c2

Browse files
authored
Improve isDeeplyNestedType for homomorphic mapped types (#56169)
1 parent 8af8f3c commit b76b4c2

File tree

5 files changed

+875
-37
lines changed

5 files changed

+875
-37
lines changed

src/compiler/checker.ts

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23568,22 +23568,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2356823568
!hasBaseType(checkClass, getDeclaringClass(p)) : false) ? undefined : checkClass;
2356923569
}
2357023570

23571-
// Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons
23572-
// for maxDepth or more occurrences or instantiations of the same type have been recorded on the given stack. The
23573-
// "sameness" of instantiations is determined by the getRecursionIdentity function. An intersection is considered
23574-
// deeply nested if any constituent of the intersection is deeply nested. It is possible, though highly unlikely, for
23575-
// the deeply nested check to be true in a situation where a chain of instantiations is not infinitely expanding.
23576-
// Effectively, we will generate a false positive when two types are structurally equal to at least maxDepth levels,
23577-
// but unequal at some level beyond that.
23578-
// In addition, this will also detect when an indexed access has been chained off of maxDepth more times (which is
23579-
// essentially the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding
23580-
// false positives for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
23581-
// It also detects when a recursive type reference has expanded maxDepth or more times, e.g. if the true branch of
23582-
// `type A<T> = null extends T ? [A<NonNullable<T>>] : [T]`
23583-
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`. In such cases we need
23584-
// to terminate the expansion, and we do so here.
23571+
// Return true if the given type is deeply nested. We consider this to be the case when the given stack contains
23572+
// maxDepth or more occurrences of types with the same recursion identity as the given type. The recursion identity
23573+
// provides a shared identity for type instantiations that repeat in some (possibly infinite) pattern. For example,
23574+
// in `type Deep<T> = { next: Deep<Deep<T>> }`, repeatedly referencing the `next` property leads to an infinite
23575+
// sequence of ever deeper instantiations with the same recursion identity (in this case the symbol associated with
23576+
// the object type literal).
23577+
// A homomorphic mapped type is considered deeply nested if its target type is deeply nested, and an intersection is
23578+
// considered deeply nested if any constituent of the intersection is deeply nested.
23579+
// It is possible, though highly unlikely, for the deeply nested check to be true in a situation where a chain of
23580+
// instantiations is not infinitely expanding. Effectively, we will generate a false positive when two types are
23581+
// structurally equal to at least maxDepth levels, but unequal at some level beyond that.
2358523582
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxDepth = 3): boolean {
2358623583
if (depth >= maxDepth) {
23584+
if ((getObjectFlags(type) & ObjectFlags.InstantiatedMapped) === ObjectFlags.InstantiatedMapped) {
23585+
type = getMappedTargetWithSymbol(type);
23586+
}
2358723587
if (type.flags & TypeFlags.Intersection) {
2358823588
return some((type as IntersectionType).types, t => isDeeplyNestedType(t, stack, depth, maxDepth));
2358923589
}
@@ -23592,7 +23592,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2359223592
let lastTypeId = 0;
2359323593
for (let i = 0; i < depth; i++) {
2359423594
const t = stack[i];
23595-
if (t.flags & TypeFlags.Intersection ? some((t as IntersectionType).types, u => getRecursionIdentity(u) === identity) : getRecursionIdentity(t) === identity) {
23595+
if (hasMatchingRecursionIdentity(t, identity)) {
2359623596
// We only count occurrences with a higher type id than the previous occurrence, since higher
2359723597
// type ids are an indicator of newer instantiations caused by recursion.
2359823598
if (t.id >= lastTypeId) {
@@ -23608,6 +23608,32 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2360823608
return false;
2360923609
}
2361023610

23611+
// Unwrap nested homomorphic mapped types and return the deepest target type that has a symbol. This better
23612+
// preserves unique type identities for mapped types applied to explicitly written object literals. For example
23613+
// in `Mapped<{ x: Mapped<{ x: Mapped<{ x: string }>}>}>`, each of the mapped type applications will have a
23614+
// unique recursion identity (that of their target object type literal) and thus avoid appearing deeply nested.
23615+
function getMappedTargetWithSymbol(type: Type) {
23616+
let target;
23617+
while (
23618+
(getObjectFlags(type) & ObjectFlags.InstantiatedMapped) === ObjectFlags.InstantiatedMapped &&
23619+
(target = getModifiersTypeFromMappedType(type as MappedType)) &&
23620+
(target.symbol || target.flags & TypeFlags.Intersection && some((target as IntersectionType).types, t => !!t.symbol))
23621+
) {
23622+
type = target;
23623+
}
23624+
return type;
23625+
}
23626+
23627+
function hasMatchingRecursionIdentity(type: Type, identity: object): boolean {
23628+
if ((getObjectFlags(type) & ObjectFlags.InstantiatedMapped) === ObjectFlags.InstantiatedMapped) {
23629+
type = getMappedTargetWithSymbol(type);
23630+
}
23631+
if (type.flags & TypeFlags.Intersection) {
23632+
return some((type as IntersectionType).types, t => hasMatchingRecursionIdentity(t, identity));
23633+
}
23634+
return getRecursionIdentity(type) === identity;
23635+
}
23636+
2361123637
// The recursion identity of a type is an object identity that is shared among multiple instantiations of the type.
2361223638
// We track recursion identities in order to identify deeply nested and possibly infinite type instantiations with
2361323639
// the same origin. For example, when type parameters are in scope in an object type such as { x: T }, all
@@ -23623,28 +23649,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2362323649
// unique AST node.
2362423650
return (type as TypeReference).node!;
2362523651
}
23626-
if (type.symbol) {
23627-
// We track object types that have a symbol by that symbol (representing the origin of the type).
23628-
if (getObjectFlags(type) & ObjectFlags.Mapped) {
23629-
// When a homomorphic mapped type is applied to a type with a symbol, we use the symbol of that
23630-
// type as the recursion identity. This is a better strategy than using the symbol of the mapped
23631-
// type, which doesn't work well for recursive mapped types.
23632-
type = getMappedTargetWithSymbol(type);
23633-
}
23634-
if (!(getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol.flags & SymbolFlags.Class)) {
23635-
// We exclude the static side of a class since it shares its symbol with the instance side.
23636-
return type.symbol;
23637-
}
23652+
if (type.symbol && !(getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol.flags & SymbolFlags.Class)) {
23653+
// We track object types that have a symbol by that symbol (representing the origin of the type), but
23654+
// exclude the static side of a class since it shares its symbol with the instance side.
23655+
return type.symbol;
2363823656
}
2363923657
if (isTupleType(type)) {
2364023658
return type.target;
2364123659
}
2364223660
}
2364323661
if (type.flags & TypeFlags.TypeParameter) {
23662+
// We use the symbol of the type parameter such that all "fresh" instantiations of that type parameter
23663+
// have the same recursion identity.
2364423664
return type.symbol;
2364523665
}
2364623666
if (type.flags & TypeFlags.IndexedAccess) {
23647-
// Identity is the leftmost object type in a chain of indexed accesses, eg, in A[P][Q] it is A
23667+
// Identity is the leftmost object type in a chain of indexed accesses, eg, in A[P1][P2][P3] it is A.
2364823668
do {
2364923669
type = (type as IndexedAccessType).objectType;
2365023670
}
@@ -23658,14 +23678,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2365823678
return type;
2365923679
}
2366023680

23661-
function getMappedTargetWithSymbol(type: Type) {
23662-
let target = type;
23663-
while ((getObjectFlags(target) & ObjectFlags.InstantiatedMapped) === ObjectFlags.InstantiatedMapped && isMappedTypeWithKeyofConstraintDeclaration(target as MappedType)) {
23664-
target = getModifiersTypeFromMappedType(target as MappedType);
23665-
}
23666-
return target.symbol ? target : type;
23667-
}
23668-
2366923681
function isPropertyIdenticalTo(sourceProp: Symbol, targetProp: Symbol): boolean {
2367023682
return compareProperties(sourceProp, targetProp, compareTypesIdentical) !== Ternary.False;
2367123683
}

tests/baselines/reference/deeplyNestedMappedTypes.errors.txt

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@ deeplyNestedMappedTypes.ts(9,7): error TS2322: Type 'Id<{ x: { y: { z: { a: { b:
44
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; }; }; }; }; }; }>'.
55
The types of 'x.y.z.a.b.c' are incompatible between these types.
66
Type 'number' is not assignable to type 'string'.
7+
deeplyNestedMappedTypes.ts(69,5): error TS2322: Type '{ level1: { level2: { foo: string; }; }; }[]' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }[]'.
8+
Type '{ level1: { level2: { foo: string; }; }; }' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }'.
9+
The types of 'level1.level2' are incompatible between these types.
10+
Property 'bar' is missing in type '{ foo: string; }' but required in type '{ foo: string; bar: string; }'.
11+
deeplyNestedMappedTypes.ts(73,5): error TS2322: Type '{ level1: { level2: { foo: string; }; }; }[]' is not assignable to type 'T'.
12+
'T' could be instantiated with an arbitrary type which could be unrelated to '{ level1: { level2: { foo: string; }; }; }[]'.
13+
deeplyNestedMappedTypes.ts(77,5): error TS2322: Type '{ level1: { level2: { foo: string; }; }; }[]' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }[]'.
14+
Type '{ level1: { level2: { foo: string; }; }; }' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }'.
15+
The types of 'level1.level2' are incompatible between these types.
16+
Property 'bar' is missing in type '{ foo: string; }' but required in type '{ foo: string; bar: string; }'.
717

818

9-
==== deeplyNestedMappedTypes.ts (2 errors) ====
19+
==== deeplyNestedMappedTypes.ts (5 errors) ====
1020
// Simplified repro from #55535
1121

1222
type Id<T> = { [K in keyof T]: Id<T[K]> };
@@ -60,4 +70,109 @@ deeplyNestedMappedTypes.ts(17,7): error TS2322: Type 'Id2<{ x: { y: { z: { a: {
6070

6171
declare const bar1: Bar1;
6272
const bar2: Bar2 = bar1; // Error expected
73+
74+
// Repro from #56138
75+
76+
export type Input = Static<typeof Input>
77+
export const Input = Type.Object({
78+
level1: Type.Object({
79+
level2: Type.Object({
80+
foo: Type.String(),
81+
})
82+
})
83+
})
84+
85+
export type Output = Static<typeof Output>
86+
export const Output = Type.Object({
87+
level1: Type.Object({
88+
level2: Type.Object({
89+
foo: Type.String(),
90+
bar: Type.String(),
91+
})
92+
})
93+
})
94+
95+
function problematicFunction1(ors: Input[]): Output[] {
96+
return ors; // Error
97+
~~~~~~
98+
!!! error TS2322: Type '{ level1: { level2: { foo: string; }; }; }[]' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }[]'.
99+
!!! error TS2322: Type '{ level1: { level2: { foo: string; }; }; }' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }'.
100+
!!! error TS2322: The types of 'level1.level2' are incompatible between these types.
101+
!!! error TS2322: Property 'bar' is missing in type '{ foo: string; }' but required in type '{ foo: string; bar: string; }'.
102+
!!! related TS2728 deeplyNestedMappedTypes.ts:63:13: 'bar' is declared here.
103+
}
104+
105+
function problematicFunction2<T extends Output[]>(ors: Input[]): T {
106+
return ors; // Error
107+
~~~~~~
108+
!!! error TS2322: Type '{ level1: { level2: { foo: string; }; }; }[]' is not assignable to type 'T'.
109+
!!! error TS2322: 'T' could be instantiated with an arbitrary type which could be unrelated to '{ level1: { level2: { foo: string; }; }; }[]'.
110+
}
111+
112+
function problematicFunction3(ors: (typeof Input.static)[]): Output[] {
113+
return ors; // Error
114+
~~~~~~
115+
!!! error TS2322: Type '{ level1: { level2: { foo: string; }; }; }[]' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }[]'.
116+
!!! error TS2322: Type '{ level1: { level2: { foo: string; }; }; }' is not assignable to type '{ level1: { level2: { foo: string; bar: string; }; }; }'.
117+
!!! error TS2322: The types of 'level1.level2' are incompatible between these types.
118+
!!! error TS2322: Property 'bar' is missing in type '{ foo: string; }' but required in type '{ foo: string; bar: string; }'.
119+
!!! related TS2728 deeplyNestedMappedTypes.ts:63:13: 'bar' is declared here.
120+
}
121+
122+
export type Evaluate<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
123+
124+
export declare const Readonly: unique symbol;
125+
export declare const Optional: unique symbol;
126+
export declare const Hint: unique symbol;
127+
export declare const Kind: unique symbol;
128+
129+
export interface TKind {
130+
[Kind]: string
131+
}
132+
export interface TSchema extends TKind {
133+
[Readonly]?: string
134+
[Optional]?: string
135+
[Hint]?: string
136+
params: unknown[]
137+
static: unknown
138+
}
139+
140+
export type TReadonlyOptional<T extends TSchema> = TOptional<T> & TReadonly<T>
141+
export type TReadonly<T extends TSchema> = T & { [Readonly]: 'Readonly' }
142+
export type TOptional<T extends TSchema> = T & { [Optional]: 'Optional' }
143+
144+
export interface TString extends TSchema {
145+
[Kind]: 'String';
146+
static: string;
147+
type: 'string';
148+
}
149+
150+
export type ReadonlyOptionalPropertyKeys<T extends TProperties> = { [K in keyof T]: T[K] extends TReadonly<TSchema> ? (T[K] extends TOptional<T[K]> ? K : never) : never }[keyof T]
151+
export type ReadonlyPropertyKeys<T extends TProperties> = { [K in keyof T]: T[K] extends TReadonly<TSchema> ? (T[K] extends TOptional<T[K]> ? never : K) : never }[keyof T]
152+
export type OptionalPropertyKeys<T extends TProperties> = { [K in keyof T]: T[K] extends TOptional<TSchema> ? (T[K] extends TReadonly<T[K]> ? never : K) : never }[keyof T]
153+
export type RequiredPropertyKeys<T extends TProperties> = keyof Omit<T, ReadonlyOptionalPropertyKeys<T> | ReadonlyPropertyKeys<T> | OptionalPropertyKeys<T>>
154+
export type PropertiesReducer<T extends TProperties, R extends Record<keyof any, unknown>> = Evaluate<(
155+
Readonly<Partial<Pick<R, ReadonlyOptionalPropertyKeys<T>>>> &
156+
Readonly<Pick<R, ReadonlyPropertyKeys<T>>> &
157+
Partial<Pick<R, OptionalPropertyKeys<T>>> &
158+
Required<Pick<R, RequiredPropertyKeys<T>>>
159+
)>
160+
export type PropertiesReduce<T extends TProperties, P extends unknown[]> = PropertiesReducer<T, {
161+
[K in keyof T]: Static<T[K], P>
162+
}>
163+
export type TPropertyKey = string | number
164+
export type TProperties = Record<TPropertyKey, TSchema>
165+
export interface TObject<T extends TProperties = TProperties> extends TSchema {
166+
[Kind]: 'Object'
167+
static: PropertiesReduce<T, this['params']>
168+
type: 'object'
169+
properties: T
170+
}
171+
172+
export type Static<T extends TSchema, P extends unknown[] = []> = (T & { params: P; })['static']
173+
174+
declare namespace Type {
175+
function Object<T extends TProperties>(object: T): TObject<T>
176+
function String(): TString
177+
}
63178

0 commit comments

Comments
 (0)