Skip to content

Commit 237c33b

Browse files
authored
Merge pull request microsoft#30109 from Microsoft/circularConstraintErrors
Consistently error on circular constraints
2 parents a6a3ae0 + ecebc9f commit 237c33b

7 files changed

Lines changed: 184 additions & 5 deletions

File tree

src/compiler/checker.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7584,7 +7584,19 @@ namespace ts {
75847584
constraintDepth++;
75857585
let result = computeBaseConstraint(getSimplifiedType(t));
75867586
constraintDepth--;
7587-
if (!popTypeResolution() || nonTerminating) {
7587+
if (!popTypeResolution()) {
7588+
if (t.flags & TypeFlags.TypeParameter) {
7589+
const errorNode = getConstraintDeclaration(<TypeParameter>t);
7590+
if (errorNode) {
7591+
const diagnostic = error(errorNode, Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(t));
7592+
if (currentNode && !isNodeDescendantOf(errorNode, currentNode) && !isNodeDescendantOf(currentNode, errorNode)) {
7593+
addRelatedInfo(diagnostic, createDiagnosticForNode(currentNode, Diagnostics.Circularity_originates_in_type_at_this_location));
7594+
}
7595+
}
7596+
}
7597+
result = circularConstraintType;
7598+
}
7599+
if (nonTerminating) {
75887600
result = circularConstraintType;
75897601
}
75907602
t.immediateBaseConstraint = result || noConstraintType;
@@ -23541,9 +23553,8 @@ namespace ts {
2354123553
checkSourceElement(node.constraint);
2354223554
checkSourceElement(node.default);
2354323555
const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node));
23544-
if (!hasNonCircularBaseConstraint(typeParameter)) {
23545-
error(getEffectiveConstraintOfTypeParameter(node), Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(typeParameter));
23546-
}
23556+
// Resolve base constraint to reveal circularity errors
23557+
getBaseConstraintOfType(typeParameter);
2354723558
if (!hasNonCircularTypeParameterDefault(typeParameter)) {
2354823559
error(node.default, Diagnostics.Type_parameter_0_has_a_circular_default, typeToString(typeParameter));
2354923560
}

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2589,6 +2589,10 @@
25892589
"category": "Error",
25902590
"code": 2750
25912591
},
2592+
"Circularity originates in type at this location.": {
2593+
"category": "Error",
2594+
"code": 2751
2595+
},
25922596

25932597
"Import declaration '{0}' is using private name '{1}'.": {
25942598
"category": "Error",

tests/baselines/reference/recursiveMappedTypes.errors.txt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ tests/cases/conformance/types/mapped/recursiveMappedTypes.ts(8,11): error TS2313
55
tests/cases/conformance/types/mapped/recursiveMappedTypes.ts(11,6): error TS2456: Type alias 'Recurse2' circularly references itself.
66
tests/cases/conformance/types/mapped/recursiveMappedTypes.ts(12,11): error TS2313: Type parameter 'K' has a circular constraint.
77
tests/cases/conformance/types/mapped/recursiveMappedTypes.ts(20,19): error TS2589: Type instantiation is excessively deep and possibly infinite.
8+
tests/cases/conformance/types/mapped/recursiveMappedTypes.ts(66,25): error TS2313: Type parameter 'P' has a circular constraint.
89

910

10-
==== tests/cases/conformance/types/mapped/recursiveMappedTypes.ts (7 errors) ====
11+
==== tests/cases/conformance/types/mapped/recursiveMappedTypes.ts (8 errors) ====
1112
// Recursive mapped types simply appear empty
1213

1314
type Recurse = {
@@ -32,6 +33,7 @@ tests/cases/conformance/types/mapped/recursiveMappedTypes.ts(20,19): error TS258
3233
[K in keyof Recurse1]: Recurse1[K]
3334
~~~~~~~~~~~~~~
3435
!!! error TS2313: Type parameter 'K' has a circular constraint.
36+
!!! related TS2751 tests/cases/conformance/types/mapped/recursiveMappedTypes.ts:8:17: Circularity originates in type at this location.
3537
}
3638

3739
// Repro from #27881
@@ -83,4 +85,25 @@ tests/cases/conformance/types/mapped/recursiveMappedTypes.ts(20,19): error TS258
8385

8486
type a = Remap1<string[]>; // string[]
8587
type b = Remap2<string[]>; // string[]
88+
89+
// Repro from #29992
90+
91+
type NonOptionalKeys<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];
92+
type Child<T> = { [P in NonOptionalKeys<T>]: T[P] }
93+
~~~~~~~~~~~~~~~~~~
94+
!!! error TS2313: Type parameter 'P' has a circular constraint.
95+
!!! related TS2751 tests/cases/conformance/types/mapped/recursiveMappedTypes.ts:79:1: Circularity originates in type at this location.
96+
97+
export interface ListWidget {
98+
"type": "list",
99+
"minimum_count": number,
100+
"maximum_count": number,
101+
"collapsable"?: boolean, //default to false, means all expanded
102+
"each": Child<ListWidget>;
103+
}
104+
105+
type ListChild = Child<ListWidget>
106+
107+
declare let x: ListChild;
108+
x.type;
86109

tests/baselines/reference/recursiveMappedTypes.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ type Remap2<T> = T extends object ? { [P in keyof T]: Remap2<T[P]>; } : T;
6060

6161
type a = Remap1<string[]>; // string[]
6262
type b = Remap2<string[]>; // string[]
63+
64+
// Repro from #29992
65+
66+
type NonOptionalKeys<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];
67+
type Child<T> = { [P in NonOptionalKeys<T>]: T[P] }
68+
69+
export interface ListWidget {
70+
"type": "list",
71+
"minimum_count": number,
72+
"maximum_count": number,
73+
"collapsable"?: boolean, //default to false, means all expanded
74+
"each": Child<ListWidget>;
75+
}
76+
77+
type ListChild = Child<ListWidget>
78+
79+
declare let x: ListChild;
80+
x.type;
6381

6482

6583
//// [recursiveMappedTypes.js]
@@ -70,9 +88,24 @@ function foo(arg) {
7088
return arg;
7189
}
7290
product.users; // (Transform<User> | Transform<Guest>)[]
91+
x.type;
7392

7493

7594
//// [recursiveMappedTypes.d.ts]
7695
export declare type Circular<T> = {
7796
[P in keyof T]: Circular<T>;
7897
};
98+
declare type NonOptionalKeys<T> = {
99+
[P in keyof T]: undefined extends T[P] ? never : P;
100+
}[keyof T];
101+
declare type Child<T> = {
102+
[P in NonOptionalKeys<T>]: T[P];
103+
};
104+
export interface ListWidget {
105+
"type": "list";
106+
"minimum_count": number;
107+
"maximum_count": number;
108+
"collapsable"?: boolean;
109+
"each": Child<ListWidget>;
110+
}
111+
export {};

tests/baselines/reference/recursiveMappedTypes.symbols

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,57 @@ type b = Remap2<string[]>; // string[]
165165
>b : Symbol(b, Decl(recursiveMappedTypes.ts, 59, 26))
166166
>Remap2 : Symbol(Remap2, Decl(recursiveMappedTypes.ts, 56, 51))
167167

168+
// Repro from #29992
169+
170+
type NonOptionalKeys<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];
171+
>NonOptionalKeys : Symbol(NonOptionalKeys, Decl(recursiveMappedTypes.ts, 60, 26))
172+
>T : Symbol(T, Decl(recursiveMappedTypes.ts, 64, 21))
173+
>P : Symbol(P, Decl(recursiveMappedTypes.ts, 64, 29))
174+
>T : Symbol(T, Decl(recursiveMappedTypes.ts, 64, 21))
175+
>T : Symbol(T, Decl(recursiveMappedTypes.ts, 64, 21))
176+
>P : Symbol(P, Decl(recursiveMappedTypes.ts, 64, 29))
177+
>P : Symbol(P, Decl(recursiveMappedTypes.ts, 64, 29))
178+
>T : Symbol(T, Decl(recursiveMappedTypes.ts, 64, 21))
179+
180+
type Child<T> = { [P in NonOptionalKeys<T>]: T[P] }
181+
>Child : Symbol(Child, Decl(recursiveMappedTypes.ts, 64, 90))
182+
>T : Symbol(T, Decl(recursiveMappedTypes.ts, 65, 11))
183+
>P : Symbol(P, Decl(recursiveMappedTypes.ts, 65, 19))
184+
>NonOptionalKeys : Symbol(NonOptionalKeys, Decl(recursiveMappedTypes.ts, 60, 26))
185+
>T : Symbol(T, Decl(recursiveMappedTypes.ts, 65, 11))
186+
>T : Symbol(T, Decl(recursiveMappedTypes.ts, 65, 11))
187+
>P : Symbol(P, Decl(recursiveMappedTypes.ts, 65, 19))
188+
189+
export interface ListWidget {
190+
>ListWidget : Symbol(ListWidget, Decl(recursiveMappedTypes.ts, 65, 51))
191+
192+
"type": "list",
193+
>"type" : Symbol(ListWidget["type"], Decl(recursiveMappedTypes.ts, 67, 29))
194+
195+
"minimum_count": number,
196+
>"minimum_count" : Symbol(ListWidget["minimum_count"], Decl(recursiveMappedTypes.ts, 68, 19))
197+
198+
"maximum_count": number,
199+
>"maximum_count" : Symbol(ListWidget["maximum_count"], Decl(recursiveMappedTypes.ts, 69, 28))
200+
201+
"collapsable"?: boolean, //default to false, means all expanded
202+
>"collapsable" : Symbol(ListWidget["collapsable"], Decl(recursiveMappedTypes.ts, 70, 28))
203+
204+
"each": Child<ListWidget>;
205+
>"each" : Symbol(ListWidget["each"], Decl(recursiveMappedTypes.ts, 71, 28))
206+
>Child : Symbol(Child, Decl(recursiveMappedTypes.ts, 64, 90))
207+
>ListWidget : Symbol(ListWidget, Decl(recursiveMappedTypes.ts, 65, 51))
208+
}
209+
210+
type ListChild = Child<ListWidget>
211+
>ListChild : Symbol(ListChild, Decl(recursiveMappedTypes.ts, 73, 1))
212+
>Child : Symbol(Child, Decl(recursiveMappedTypes.ts, 64, 90))
213+
>ListWidget : Symbol(ListWidget, Decl(recursiveMappedTypes.ts, 65, 51))
214+
215+
declare let x: ListChild;
216+
>x : Symbol(x, Decl(recursiveMappedTypes.ts, 77, 11))
217+
>ListChild : Symbol(ListChild, Decl(recursiveMappedTypes.ts, 73, 1))
218+
219+
x.type;
220+
>x : Symbol(x, Decl(recursiveMappedTypes.ts, 77, 11))
221+

tests/baselines/reference/recursiveMappedTypes.types

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,39 @@ type a = Remap1<string[]>; // string[]
9797
type b = Remap2<string[]>; // string[]
9898
>b : string[]
9999

100+
// Repro from #29992
101+
102+
type NonOptionalKeys<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];
103+
>NonOptionalKeys : { [P in keyof T]: undefined extends T[P] ? never : P; }[keyof T]
104+
105+
type Child<T> = { [P in NonOptionalKeys<T>]: T[P] }
106+
>Child : Child<T>
107+
108+
export interface ListWidget {
109+
"type": "list",
110+
>"type" : "list"
111+
112+
"minimum_count": number,
113+
>"minimum_count" : number
114+
115+
"maximum_count": number,
116+
>"maximum_count" : number
117+
118+
"collapsable"?: boolean, //default to false, means all expanded
119+
>"collapsable" : boolean
120+
121+
"each": Child<ListWidget>;
122+
>"each" : Child<ListWidget>
123+
}
124+
125+
type ListChild = Child<ListWidget>
126+
>ListChild : Child<ListWidget>
127+
128+
declare let x: ListChild;
129+
>x : Child<ListWidget>
130+
131+
x.type;
132+
>x.type : any
133+
>x : Child<ListWidget>
134+
>type : any
135+

tests/cases/conformance/types/mapped/recursiveMappedTypes.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,21 @@ type Remap2<T> = T extends object ? { [P in keyof T]: Remap2<T[P]>; } : T;
6161

6262
type a = Remap1<string[]>; // string[]
6363
type b = Remap2<string[]>; // string[]
64+
65+
// Repro from #29992
66+
67+
type NonOptionalKeys<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];
68+
type Child<T> = { [P in NonOptionalKeys<T>]: T[P] }
69+
70+
export interface ListWidget {
71+
"type": "list",
72+
"minimum_count": number,
73+
"maximum_count": number,
74+
"collapsable"?: boolean, //default to false, means all expanded
75+
"each": Child<ListWidget>;
76+
}
77+
78+
type ListChild = Child<ListWidget>
79+
80+
declare let x: ListChild;
81+
x.type;

0 commit comments

Comments
 (0)