Skip to content

Commit 5ef8a29

Browse files
ahejlsbergJack-Works
authored andcommitted
Constraints for mapped types with filtering 'as' clauses (microsoft#48699)
* Constraints for mapped types with filtering 'as' clauses * Add regression test
1 parent 444fc99 commit 5ef8a29

File tree

6 files changed

+225
-9
lines changed

6 files changed

+225
-9
lines changed

src/compiler/checker.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -15737,11 +15737,14 @@ namespace ts {
1573715737
return type[cache] = elementType;
1573815738
}
1573915739
}
15740-
// If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper
15741-
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
15742-
// construct the type Box<T[X]>.
15743-
if (isGenericMappedType(objectType) && !objectType.declaration.nameType) {
15744-
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
15740+
// If the object type is a mapped type { [P in K]: E }, where K is generic, or { [P in K as N]: E }, where
15741+
// K is generic and N is assignable to P, instantiate E using a mapper that substitutes the index type for P.
15742+
// For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the type Box<T[X]>.
15743+
if (isGenericMappedType(objectType)) {
15744+
const nameType = getNameTypeFromMappedType(objectType);
15745+
if (!nameType || isTypeAssignableTo(nameType, getTypeParameterFromMappedType(objectType))) {
15746+
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
15747+
}
1574515748
}
1574615749
return type[cache] = type;
1574715750
}

tests/baselines/reference/mappedTypeConstraints2.errors.txt

+22
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,26 @@ tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts(25,57): error TS2
4141
~~~~~~~~~~~~~~
4242
!!! error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
4343
!!! error TS2322: 'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.
44+
45+
// Repro from #48626
46+
47+
interface Bounds {
48+
min: number;
49+
max: number;
50+
}
51+
52+
type NumericBoundsOf<T> = {
53+
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
54+
}
55+
56+
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
57+
for (const [key, val] of Object.entries(obj)) {
58+
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
59+
if (boundsForKey) {
60+
const { min, max } = boundsForKey;
61+
if (min > val || max < val) return false;
62+
}
63+
}
64+
return true;
65+
}
4466

tests/baselines/reference/mappedTypeConstraints2.js

+45-4
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,53 @@ type Foo<T extends string> = {
2424
};
2525

2626
const get = <T extends string>(t: T, foo: Foo<T>): T => foo[`get${t}`]; // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
27+
28+
// Repro from #48626
29+
30+
interface Bounds {
31+
min: number;
32+
max: number;
33+
}
34+
35+
type NumericBoundsOf<T> = {
36+
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
37+
}
38+
39+
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
40+
for (const [key, val] of Object.entries(obj)) {
41+
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
42+
if (boundsForKey) {
43+
const { min, max } = boundsForKey;
44+
if (min > val || max < val) return false;
45+
}
46+
}
47+
return true;
48+
}
2749

2850

2951
//// [mappedTypeConstraints2.js]
3052
"use strict";
3153
function f1(obj, key) {
32-
var x = obj[key];
54+
const x = obj[key];
3355
}
3456
function f2(obj, key) {
35-
var x = obj[key]; // Error
57+
const x = obj[key]; // Error
3658
}
3759
function f3(obj, key) {
38-
var x = obj[key]; // Error
60+
const x = obj[key]; // Error
61+
}
62+
const get = (t, foo) => foo[`get${t}`]; // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
63+
function validate(obj, bounds) {
64+
for (const [key, val] of Object.entries(obj)) {
65+
const boundsForKey = bounds[key];
66+
if (boundsForKey) {
67+
const { min, max } = boundsForKey;
68+
if (min > val || max < val)
69+
return false;
70+
}
71+
}
72+
return true;
3973
}
40-
var get = function (t, foo) { return foo["get".concat(t)]; }; // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
4174

4275

4376
//// [mappedTypeConstraints2.d.ts]
@@ -63,3 +96,11 @@ declare type Foo<T extends string> = {
6396
[RemappedT in T as `get${RemappedT}`]: RemappedT;
6497
};
6598
declare const get: <T extends string>(t: T, foo: Foo<T>) => T;
99+
interface Bounds {
100+
min: number;
101+
max: number;
102+
}
103+
declare type NumericBoundsOf<T> = {
104+
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
105+
};
106+
declare function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>): boolean;

tests/baselines/reference/mappedTypeConstraints2.symbols

+67
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,70 @@ const get = <T extends string>(t: T, foo: Foo<T>): T => foo[`get${t}`]; // Type
104104
>foo : Symbol(foo, Decl(mappedTypeConstraints2.ts, 24, 36))
105105
>t : Symbol(t, Decl(mappedTypeConstraints2.ts, 24, 31))
106106

107+
// Repro from #48626
108+
109+
interface Bounds {
110+
>Bounds : Symbol(Bounds, Decl(mappedTypeConstraints2.ts, 24, 71))
111+
112+
min: number;
113+
>min : Symbol(Bounds.min, Decl(mappedTypeConstraints2.ts, 28, 18))
114+
115+
max: number;
116+
>max : Symbol(Bounds.max, Decl(mappedTypeConstraints2.ts, 29, 16))
117+
}
118+
119+
type NumericBoundsOf<T> = {
120+
>NumericBoundsOf : Symbol(NumericBoundsOf, Decl(mappedTypeConstraints2.ts, 31, 1))
121+
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 33, 21))
122+
123+
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
124+
>K : Symbol(K, Decl(mappedTypeConstraints2.ts, 34, 5))
125+
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 33, 21))
126+
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 33, 21))
127+
>K : Symbol(K, Decl(mappedTypeConstraints2.ts, 34, 5))
128+
>K : Symbol(K, Decl(mappedTypeConstraints2.ts, 34, 5))
129+
>Bounds : Symbol(Bounds, Decl(mappedTypeConstraints2.ts, 24, 71))
130+
}
131+
132+
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
133+
>validate : Symbol(validate, Decl(mappedTypeConstraints2.ts, 35, 1))
134+
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
135+
>obj : Symbol(obj, Decl(mappedTypeConstraints2.ts, 37, 36))
136+
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
137+
>bounds : Symbol(bounds, Decl(mappedTypeConstraints2.ts, 37, 43))
138+
>NumericBoundsOf : Symbol(NumericBoundsOf, Decl(mappedTypeConstraints2.ts, 31, 1))
139+
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
140+
141+
for (const [key, val] of Object.entries(obj)) {
142+
>key : Symbol(key, Decl(mappedTypeConstraints2.ts, 38, 16))
143+
>val : Symbol(val, Decl(mappedTypeConstraints2.ts, 38, 20))
144+
>Object.entries : Symbol(ObjectConstructor.entries, Decl(lib.es2017.object.d.ts, --, --), Decl(lib.es2017.object.d.ts, --, --))
145+
>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
146+
>entries : Symbol(ObjectConstructor.entries, Decl(lib.es2017.object.d.ts, --, --), Decl(lib.es2017.object.d.ts, --, --))
147+
>obj : Symbol(obj, Decl(mappedTypeConstraints2.ts, 37, 36))
148+
149+
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
150+
>boundsForKey : Symbol(boundsForKey, Decl(mappedTypeConstraints2.ts, 39, 13))
151+
>bounds : Symbol(bounds, Decl(mappedTypeConstraints2.ts, 37, 43))
152+
>key : Symbol(key, Decl(mappedTypeConstraints2.ts, 38, 16))
153+
>NumericBoundsOf : Symbol(NumericBoundsOf, Decl(mappedTypeConstraints2.ts, 31, 1))
154+
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
155+
156+
if (boundsForKey) {
157+
>boundsForKey : Symbol(boundsForKey, Decl(mappedTypeConstraints2.ts, 39, 13))
158+
159+
const { min, max } = boundsForKey;
160+
>min : Symbol(min, Decl(mappedTypeConstraints2.ts, 41, 19))
161+
>max : Symbol(max, Decl(mappedTypeConstraints2.ts, 41, 24))
162+
>boundsForKey : Symbol(boundsForKey, Decl(mappedTypeConstraints2.ts, 39, 13))
163+
164+
if (min > val || max < val) return false;
165+
>min : Symbol(min, Decl(mappedTypeConstraints2.ts, 41, 19))
166+
>val : Symbol(val, Decl(mappedTypeConstraints2.ts, 38, 20))
167+
>max : Symbol(max, Decl(mappedTypeConstraints2.ts, 41, 24))
168+
>val : Symbol(val, Decl(mappedTypeConstraints2.ts, 38, 20))
169+
}
170+
}
171+
return true;
172+
}
173+

tests/baselines/reference/mappedTypeConstraints2.types

+60
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,63 @@ const get = <T extends string>(t: T, foo: Foo<T>): T => foo[`get${t}`]; // Type
6868
>`get${t}` : `get${T}`
6969
>t : T
7070

71+
// Repro from #48626
72+
73+
interface Bounds {
74+
min: number;
75+
>min : number
76+
77+
max: number;
78+
>max : number
79+
}
80+
81+
type NumericBoundsOf<T> = {
82+
>NumericBoundsOf : NumericBoundsOf<T>
83+
84+
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
85+
}
86+
87+
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
88+
>validate : <T extends object>(obj: T, bounds: NumericBoundsOf<T>) => boolean
89+
>obj : T
90+
>bounds : NumericBoundsOf<T>
91+
92+
for (const [key, val] of Object.entries(obj)) {
93+
>key : string
94+
>val : any
95+
>Object.entries(obj) : [string, any][]
96+
>Object.entries : { <T>(o: { [s: string]: T; } | ArrayLike<T>): [string, T][]; (o: {}): [string, any][]; }
97+
>Object : ObjectConstructor
98+
>entries : { <T>(o: { [s: string]: T; } | ArrayLike<T>): [string, T][]; (o: {}): [string, any][]; }
99+
>obj : T
100+
101+
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
102+
>boundsForKey : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
103+
>bounds[key as keyof NumericBoundsOf<T>] : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
104+
>bounds : NumericBoundsOf<T>
105+
>key as keyof NumericBoundsOf<T> : keyof NumericBoundsOf<T>
106+
>key : string
107+
108+
if (boundsForKey) {
109+
>boundsForKey : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
110+
111+
const { min, max } = boundsForKey;
112+
>min : number
113+
>max : number
114+
>boundsForKey : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
115+
116+
if (min > val || max < val) return false;
117+
>min > val || max < val : boolean
118+
>min > val : boolean
119+
>min : number
120+
>val : any
121+
>max < val : boolean
122+
>max : number
123+
>val : any
124+
>false : false
125+
}
126+
}
127+
return true;
128+
>true : true
129+
}
130+

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

+23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @strict: true
22
// @declaration: true
3+
// @target: es2017
34

45
type Mapped1<K extends string> = { [P in K]: { a: P } };
56

@@ -26,3 +27,25 @@ type Foo<T extends string> = {
2627
};
2728

2829
const get = <T extends string>(t: T, foo: Foo<T>): T => foo[`get${t}`]; // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
30+
31+
// Repro from #48626
32+
33+
interface Bounds {
34+
min: number;
35+
max: number;
36+
}
37+
38+
type NumericBoundsOf<T> = {
39+
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
40+
}
41+
42+
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
43+
for (const [key, val] of Object.entries(obj)) {
44+
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
45+
if (boundsForKey) {
46+
const { min, max } = boundsForKey;
47+
if (min > val || max < val) return false;
48+
}
49+
}
50+
return true;
51+
}

0 commit comments

Comments
 (0)