Constraints for mapped types with filtering 'as' clauses (#48699)

* Constraints for mapped types with filtering 'as' clauses

* Add regression test
This commit is contained in:
Anders Hejlsberg
2022-04-18 14:24:22 -07:00
committed by GitHub
parent 56a4a93718
commit 65f6cb23d3
6 changed files with 225 additions and 9 deletions

View File

@@ -15737,11 +15737,14 @@ namespace ts {
return type[cache] = elementType;
}
}
// If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
// construct the type Box<T[X]>.
if (isGenericMappedType(objectType) && !objectType.declaration.nameType) {
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
// If the object type is a mapped type { [P in K]: E }, where K is generic, or { [P in K as N]: E }, where
// K is generic and N is assignable to P, instantiate E using a mapper that substitutes the index type for P.
// For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the type Box<T[X]>.
if (isGenericMappedType(objectType)) {
const nameType = getNameTypeFromMappedType(objectType);
if (!nameType || isTypeAssignableTo(nameType, getTypeParameterFromMappedType(objectType))) {
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
}
}
return type[cache] = type;
}

View File

@@ -41,4 +41,26 @@ tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts(25,57): error TS2
~~~~~~~~~~~~~~
!!! error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
!!! error TS2322: 'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.
// Repro from #48626
interface Bounds {
min: number;
max: number;
}
type NumericBoundsOf<T> = {
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
}
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
for (const [key, val] of Object.entries(obj)) {
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
if (boundsForKey) {
const { min, max } = boundsForKey;
if (min > val || max < val) return false;
}
}
return true;
}

View File

@@ -24,20 +24,53 @@ type Foo<T extends string> = {
};
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'
// Repro from #48626
interface Bounds {
min: number;
max: number;
}
type NumericBoundsOf<T> = {
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
}
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
for (const [key, val] of Object.entries(obj)) {
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
if (boundsForKey) {
const { min, max } = boundsForKey;
if (min > val || max < val) return false;
}
}
return true;
}
//// [mappedTypeConstraints2.js]
"use strict";
function f1(obj, key) {
var x = obj[key];
const x = obj[key];
}
function f2(obj, key) {
var x = obj[key]; // Error
const x = obj[key]; // Error
}
function f3(obj, key) {
var x = obj[key]; // Error
const x = obj[key]; // Error
}
const get = (t, foo) => foo[`get${t}`]; // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
function validate(obj, bounds) {
for (const [key, val] of Object.entries(obj)) {
const boundsForKey = bounds[key];
if (boundsForKey) {
const { min, max } = boundsForKey;
if (min > val || max < val)
return false;
}
}
return true;
}
var get = function (t, foo) { return foo["get".concat(t)]; }; // Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'
//// [mappedTypeConstraints2.d.ts]
@@ -63,3 +96,11 @@ declare type Foo<T extends string> = {
[RemappedT in T as `get${RemappedT}`]: RemappedT;
};
declare const get: <T extends string>(t: T, foo: Foo<T>) => T;
interface Bounds {
min: number;
max: number;
}
declare type NumericBoundsOf<T> = {
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
};
declare function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>): boolean;

View File

@@ -104,3 +104,70 @@ const get = <T extends string>(t: T, foo: Foo<T>): T => foo[`get${t}`]; // Type
>foo : Symbol(foo, Decl(mappedTypeConstraints2.ts, 24, 36))
>t : Symbol(t, Decl(mappedTypeConstraints2.ts, 24, 31))
// Repro from #48626
interface Bounds {
>Bounds : Symbol(Bounds, Decl(mappedTypeConstraints2.ts, 24, 71))
min: number;
>min : Symbol(Bounds.min, Decl(mappedTypeConstraints2.ts, 28, 18))
max: number;
>max : Symbol(Bounds.max, Decl(mappedTypeConstraints2.ts, 29, 16))
}
type NumericBoundsOf<T> = {
>NumericBoundsOf : Symbol(NumericBoundsOf, Decl(mappedTypeConstraints2.ts, 31, 1))
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 33, 21))
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
>K : Symbol(K, Decl(mappedTypeConstraints2.ts, 34, 5))
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 33, 21))
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 33, 21))
>K : Symbol(K, Decl(mappedTypeConstraints2.ts, 34, 5))
>K : Symbol(K, Decl(mappedTypeConstraints2.ts, 34, 5))
>Bounds : Symbol(Bounds, Decl(mappedTypeConstraints2.ts, 24, 71))
}
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
>validate : Symbol(validate, Decl(mappedTypeConstraints2.ts, 35, 1))
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
>obj : Symbol(obj, Decl(mappedTypeConstraints2.ts, 37, 36))
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
>bounds : Symbol(bounds, Decl(mappedTypeConstraints2.ts, 37, 43))
>NumericBoundsOf : Symbol(NumericBoundsOf, Decl(mappedTypeConstraints2.ts, 31, 1))
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
for (const [key, val] of Object.entries(obj)) {
>key : Symbol(key, Decl(mappedTypeConstraints2.ts, 38, 16))
>val : Symbol(val, Decl(mappedTypeConstraints2.ts, 38, 20))
>Object.entries : Symbol(ObjectConstructor.entries, Decl(lib.es2017.object.d.ts, --, --), Decl(lib.es2017.object.d.ts, --, --))
>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>entries : Symbol(ObjectConstructor.entries, Decl(lib.es2017.object.d.ts, --, --), Decl(lib.es2017.object.d.ts, --, --))
>obj : Symbol(obj, Decl(mappedTypeConstraints2.ts, 37, 36))
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
>boundsForKey : Symbol(boundsForKey, Decl(mappedTypeConstraints2.ts, 39, 13))
>bounds : Symbol(bounds, Decl(mappedTypeConstraints2.ts, 37, 43))
>key : Symbol(key, Decl(mappedTypeConstraints2.ts, 38, 16))
>NumericBoundsOf : Symbol(NumericBoundsOf, Decl(mappedTypeConstraints2.ts, 31, 1))
>T : Symbol(T, Decl(mappedTypeConstraints2.ts, 37, 18))
if (boundsForKey) {
>boundsForKey : Symbol(boundsForKey, Decl(mappedTypeConstraints2.ts, 39, 13))
const { min, max } = boundsForKey;
>min : Symbol(min, Decl(mappedTypeConstraints2.ts, 41, 19))
>max : Symbol(max, Decl(mappedTypeConstraints2.ts, 41, 24))
>boundsForKey : Symbol(boundsForKey, Decl(mappedTypeConstraints2.ts, 39, 13))
if (min > val || max < val) return false;
>min : Symbol(min, Decl(mappedTypeConstraints2.ts, 41, 19))
>val : Symbol(val, Decl(mappedTypeConstraints2.ts, 38, 20))
>max : Symbol(max, Decl(mappedTypeConstraints2.ts, 41, 24))
>val : Symbol(val, Decl(mappedTypeConstraints2.ts, 38, 20))
}
}
return true;
}

View File

@@ -68,3 +68,63 @@ const get = <T extends string>(t: T, foo: Foo<T>): T => foo[`get${t}`]; // Type
>`get${t}` : `get${T}`
>t : T
// Repro from #48626
interface Bounds {
min: number;
>min : number
max: number;
>max : number
}
type NumericBoundsOf<T> = {
>NumericBoundsOf : NumericBoundsOf<T>
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
}
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
>validate : <T extends object>(obj: T, bounds: NumericBoundsOf<T>) => boolean
>obj : T
>bounds : NumericBoundsOf<T>
for (const [key, val] of Object.entries(obj)) {
>key : string
>val : any
>Object.entries(obj) : [string, any][]
>Object.entries : { <T>(o: { [s: string]: T; } | ArrayLike<T>): [string, T][]; (o: {}): [string, any][]; }
>Object : ObjectConstructor
>entries : { <T>(o: { [s: string]: T; } | ArrayLike<T>): [string, T][]; (o: {}): [string, any][]; }
>obj : T
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
>boundsForKey : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
>bounds[key as keyof NumericBoundsOf<T>] : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
>bounds : NumericBoundsOf<T>
>key as keyof NumericBoundsOf<T> : keyof NumericBoundsOf<T>
>key : string
if (boundsForKey) {
>boundsForKey : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
const { min, max } = boundsForKey;
>min : number
>max : number
>boundsForKey : NumericBoundsOf<T>[keyof NumericBoundsOf<T>]
if (min > val || max < val) return false;
>min > val || max < val : boolean
>min > val : boolean
>min : number
>val : any
>max < val : boolean
>max : number
>val : any
>false : false
}
}
return true;
>true : true
}

View File

@@ -1,5 +1,6 @@
// @strict: true
// @declaration: true
// @target: es2017
type Mapped1<K extends string> = { [P in K]: { a: P } };
@@ -26,3 +27,25 @@ type Foo<T extends string> = {
};
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'
// Repro from #48626
interface Bounds {
min: number;
max: number;
}
type NumericBoundsOf<T> = {
[K in keyof T as T[K] extends number | undefined ? K : never]: Bounds;
}
function validate<T extends object>(obj: T, bounds: NumericBoundsOf<T>) {
for (const [key, val] of Object.entries(obj)) {
const boundsForKey = bounds[key as keyof NumericBoundsOf<T>];
if (boundsForKey) {
const { min, max } = boundsForKey;
if (min > val || max < val) return false;
}
}
return true;
}