diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7a75d6e925f8f..3f5f51acd7179 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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 }[X], we - // construct the type Box. - 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 }[X], we construct the type Box. + 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; } diff --git a/tests/baselines/reference/mappedTypeConstraints2.errors.txt b/tests/baselines/reference/mappedTypeConstraints2.errors.txt index 5dcfca6d63f5d..7950e9d0f5494 100644 --- a/tests/baselines/reference/mappedTypeConstraints2.errors.txt +++ b/tests/baselines/reference/mappedTypeConstraints2.errors.txt @@ -41,4 +41,26 @@ tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts(25,57): error TS2 ~~~~~~~~~~~~~~ !!! error TS2322: Type 'Foo[`get${T}`]' is not assignable to type 'T'. !!! error TS2322: 'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo[`get${T}`]'. + + // Repro from #48626 + + interface Bounds { + min: number; + max: number; + } + + type NumericBoundsOf = { + [K in keyof T as T[K] extends number | undefined ? K : never]: Bounds; + } + + function validate(obj: T, bounds: NumericBoundsOf) { + for (const [key, val] of Object.entries(obj)) { + const boundsForKey = bounds[key as keyof NumericBoundsOf]; + if (boundsForKey) { + const { min, max } = boundsForKey; + if (min > val || max < val) return false; + } + } + return true; + } \ No newline at end of file diff --git a/tests/baselines/reference/mappedTypeConstraints2.js b/tests/baselines/reference/mappedTypeConstraints2.js index 4c583b3249538..3237f1d8b8107 100644 --- a/tests/baselines/reference/mappedTypeConstraints2.js +++ b/tests/baselines/reference/mappedTypeConstraints2.js @@ -24,20 +24,53 @@ type Foo = { }; const get = (t: T, foo: Foo): T => foo[`get${t}`]; // Type 'Foo[`get${T}`]' is not assignable to type 'T' + +// Repro from #48626 + +interface Bounds { + min: number; + max: number; +} + +type NumericBoundsOf = { + [K in keyof T as T[K] extends number | undefined ? K : never]: Bounds; +} + +function validate(obj: T, bounds: NumericBoundsOf) { + for (const [key, val] of Object.entries(obj)) { + const boundsForKey = bounds[key as keyof NumericBoundsOf]; + 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[`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[`get${T}`]' is not assignable to type 'T' //// [mappedTypeConstraints2.d.ts] @@ -63,3 +96,11 @@ declare type Foo = { [RemappedT in T as `get${RemappedT}`]: RemappedT; }; declare const get: (t: T, foo: Foo) => T; +interface Bounds { + min: number; + max: number; +} +declare type NumericBoundsOf = { + [K in keyof T as T[K] extends number | undefined ? K : never]: Bounds; +}; +declare function validate(obj: T, bounds: NumericBoundsOf): boolean; diff --git a/tests/baselines/reference/mappedTypeConstraints2.symbols b/tests/baselines/reference/mappedTypeConstraints2.symbols index 9e1727cae64e0..90ee329dada27 100644 --- a/tests/baselines/reference/mappedTypeConstraints2.symbols +++ b/tests/baselines/reference/mappedTypeConstraints2.symbols @@ -104,3 +104,70 @@ const get = (t: T, foo: Foo): 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 = { +>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(obj: T, bounds: NumericBoundsOf) { +>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]; +>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; +} + diff --git a/tests/baselines/reference/mappedTypeConstraints2.types b/tests/baselines/reference/mappedTypeConstraints2.types index e1967ee345308..534e02c8518db 100644 --- a/tests/baselines/reference/mappedTypeConstraints2.types +++ b/tests/baselines/reference/mappedTypeConstraints2.types @@ -68,3 +68,63 @@ const get = (t: T, foo: Foo): 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 = { +>NumericBoundsOf : NumericBoundsOf + + [K in keyof T as T[K] extends number | undefined ? K : never]: Bounds; +} + +function validate(obj: T, bounds: NumericBoundsOf) { +>validate : (obj: T, bounds: NumericBoundsOf) => boolean +>obj : T +>bounds : NumericBoundsOf + + for (const [key, val] of Object.entries(obj)) { +>key : string +>val : any +>Object.entries(obj) : [string, any][] +>Object.entries : { (o: { [s: string]: T; } | ArrayLike): [string, T][]; (o: {}): [string, any][]; } +>Object : ObjectConstructor +>entries : { (o: { [s: string]: T; } | ArrayLike): [string, T][]; (o: {}): [string, any][]; } +>obj : T + + const boundsForKey = bounds[key as keyof NumericBoundsOf]; +>boundsForKey : NumericBoundsOf[keyof NumericBoundsOf] +>bounds[key as keyof NumericBoundsOf] : NumericBoundsOf[keyof NumericBoundsOf] +>bounds : NumericBoundsOf +>key as keyof NumericBoundsOf : keyof NumericBoundsOf +>key : string + + if (boundsForKey) { +>boundsForKey : NumericBoundsOf[keyof NumericBoundsOf] + + const { min, max } = boundsForKey; +>min : number +>max : number +>boundsForKey : NumericBoundsOf[keyof NumericBoundsOf] + + 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 +} + diff --git a/tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts b/tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts index b77abb21dc94c..d0f1703b1138e 100644 --- a/tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts +++ b/tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts @@ -1,5 +1,6 @@ // @strict: true // @declaration: true +// @target: es2017 type Mapped1 = { [P in K]: { a: P } }; @@ -26,3 +27,25 @@ type Foo = { }; const get = (t: T, foo: Foo): T => foo[`get${t}`]; // Type 'Foo[`get${T}`]' is not assignable to type 'T' + +// Repro from #48626 + +interface Bounds { + min: number; + max: number; +} + +type NumericBoundsOf = { + [K in keyof T as T[K] extends number | undefined ? K : never]: Bounds; +} + +function validate(obj: T, bounds: NumericBoundsOf) { + for (const [key, val] of Object.entries(obj)) { + const boundsForKey = bounds[key as keyof NumericBoundsOf]; + if (boundsForKey) { + const { min, max } = boundsForKey; + if (min > val || max < val) return false; + } + } + return true; +}