Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 18, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 22 additions & 0 deletions tests/baselines/reference/mappedTypeConstraints2.errors.txt
Original file line number Diff line number Diff line change
@@ -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;
}

49 changes: 45 additions & 4 deletions tests/baselines/reference/mappedTypeConstraints2.js
Original file line number Diff line number Diff line change
@@ -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;
67 changes: 67 additions & 0 deletions tests/baselines/reference/mappedTypeConstraints2.symbols
Original file line number Diff line number Diff line change
@@ -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;
}

60 changes: 60 additions & 0 deletions tests/baselines/reference/mappedTypeConstraints2.types
Original file line number Diff line number Diff line change
@@ -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
}

23 changes: 23 additions & 0 deletions tests/cases/conformance/types/mapped/mappedTypeConstraints2.ts
Original file line number Diff line number Diff line change
@@ -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;
}