Skip to content

Commit 10a9bb6

Browse files
committed
Infer from filtering mapped types
1 parent f2df10f commit 10a9bb6

File tree

5 files changed

+326
-9
lines changed

5 files changed

+326
-9
lines changed

src/compiler/checker.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13174,8 +13174,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1317413174
// and T as the template type.
1317513175
const typeParameter = getTypeParameterFromMappedType(type);
1317613176
const constraintType = getConstraintTypeFromMappedType(type);
13177-
const nameType = getNameTypeFromMappedType(type.target as MappedType || type);
13178-
const isFilteringMappedType = nameType && isTypeAssignableTo(nameType, typeParameter);
13177+
const mappedType = (type.target as MappedType) || type;
13178+
const nameType = getNameTypeFromMappedType(mappedType);
13179+
const shouldLinkPropDeclarations = !nameType || isFilteringMappedType(mappedType);
1317913180
const templateType = getTemplateTypeFromMappedType(type.target as MappedType || type);
1318013181
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
1318113182
const templateModifiers = getMappedTypeModifiers(type);
@@ -13222,7 +13223,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1322213223
prop.links.keyType = keyType;
1322313224
if (modifiersProp) {
1322413225
prop.links.syntheticOrigin = modifiersProp;
13225-
prop.declarations = !nameType || isFilteringMappedType ? modifiersProp.declarations : undefined;
13226+
prop.declarations = shouldLinkPropDeclarations ? modifiersProp.declarations : undefined;
1322613227
}
1322713228
members.set(propName, prop);
1322813229
}
@@ -13355,6 +13356,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1335513356
return false;
1335613357
}
1335713358

13359+
function isFilteringMappedType(type: MappedType): boolean {
13360+
const nameType = getNameTypeFromMappedType(type);
13361+
return !!nameType && isTypeAssignableTo(nameType, getTypeParameterFromMappedType(type));
13362+
}
13363+
1335813364
function resolveStructuredTypeMembers(type: StructuredType): ResolvedType {
1335913365
if (!(type as ResolvedType).members) {
1336013366
if (type.flags & TypeFlags.Object) {
@@ -17473,8 +17479,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1747317479
// K is generic and N is assignable to P, instantiate E using a mapper that substitutes the index type for P.
1747417480
// For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the type Box<T[X]>.
1747517481
if (isGenericMappedType(objectType)) {
17476-
const nameType = getNameTypeFromMappedType(objectType);
17477-
if (!nameType || isTypeAssignableTo(nameType, getTypeParameterFromMappedType(objectType))) {
17482+
if (!getNameTypeFromMappedType(objectType) || isFilteringMappedType(objectType)) {
1747817483
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
1747917484
}
1748017485
}
@@ -24730,10 +24735,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2473024735
const targetNameType = getNameTypeFromMappedType(target);
2473124736
if (sourceNameType && targetNameType) inferFromTypes(sourceNameType, targetNameType);
2473224737
}
24733-
if (getObjectFlags(target) & ObjectFlags.Mapped && !(target as MappedType).declaration.nameType) {
24734-
const constraintType = getConstraintTypeFromMappedType(target as MappedType);
24735-
if (inferToMappedType(source, target as MappedType, constraintType)) {
24736-
return;
24738+
if (getObjectFlags(target) & ObjectFlags.Mapped) {
24739+
const mappedType = target as MappedType;
24740+
if (!getNameTypeFromMappedType(mappedType) || isFilteringMappedType(mappedType)) {
24741+
const constraintType = getConstraintTypeFromMappedType(mappedType);
24742+
if (inferToMappedType(source, mappedType, constraintType)) {
24743+
return;
24744+
}
2473724745
}
2473824746
}
2473924747
// Infer from the members of source and target only if the two types are possibly related
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
tests/cases/compiler/reverseMappedTypeInferFromFilteringNameType.ts(34,7): error TS2322: Type '{ last: true; doesntExist: true; }' is not assignable to type 'AllowedNeeds<{ last: boolean; doesntExist: boolean; }>'.
2+
Object literal may only specify known properties, and 'doesntExist' does not exist in type 'AllowedNeeds<{ last: boolean; doesntExist: boolean; }>'.
3+
4+
5+
==== tests/cases/compiler/reverseMappedTypeInferFromFilteringNameType.ts (1 errors) ====
6+
declare class User {
7+
public name: string;
8+
public last: string;
9+
public age: number;
10+
}
11+
12+
type AllowedNeeds<T> = {
13+
[K in keyof T as K & keyof User]: T[K];
14+
};
15+
16+
declare function extend<T>(
17+
input: {
18+
[K in keyof T]: {
19+
needs: AllowedNeeds<T[K]>
20+
compute: (x: Pick<User, keyof T[K] & keyof User>) => any;
21+
};
22+
}
23+
): T
24+
25+
const inferred1 = extend({
26+
fullName: {
27+
needs: {
28+
name: true,
29+
last: true,
30+
},
31+
compute: (user) => `${user.name} ${user.last}`,
32+
},
33+
});
34+
35+
const inferred2 = extend({
36+
fullName: {
37+
needs: {
38+
last: true,
39+
doesntExist: true // error
40+
~~~~~~~~~~~~~~~~~
41+
!!! error TS2322: Type '{ last: true; doesntExist: true; }' is not assignable to type 'AllowedNeeds<{ last: boolean; doesntExist: boolean; }>'.
42+
!!! error TS2322: Object literal may only specify known properties, and 'doesntExist' does not exist in type 'AllowedNeeds<{ last: boolean; doesntExist: boolean; }>'.
43+
!!! related TS6500 tests/cases/compiler/reverseMappedTypeInferFromFilteringNameType.ts:14:7: The expected type comes from property 'needs' which is declared here on type '{ needs: AllowedNeeds<{ last: boolean; doesntExist: boolean; }>; compute: (x: Pick<User, "last">) => any; }'
44+
},
45+
compute: (user) => {},
46+
},
47+
});
48+
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
=== tests/cases/compiler/reverseMappedTypeInferFromFilteringNameType.ts ===
2+
declare class User {
3+
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 0, 0))
4+
5+
public name: string;
6+
>name : Symbol(User.name, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 0, 20))
7+
8+
public last: string;
9+
>last : Symbol(User.last, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 1, 22))
10+
11+
public age: number;
12+
>age : Symbol(User.age, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 2, 22))
13+
}
14+
15+
type AllowedNeeds<T> = {
16+
>AllowedNeeds : Symbol(AllowedNeeds, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 4, 1))
17+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 6, 18))
18+
19+
[K in keyof T as K & keyof User]: T[K];
20+
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 7, 3))
21+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 6, 18))
22+
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 7, 3))
23+
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 0, 0))
24+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 6, 18))
25+
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 7, 3))
26+
27+
};
28+
29+
declare function extend<T>(
30+
>extend : Symbol(extend, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 8, 2))
31+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 10, 24))
32+
33+
input: {
34+
>input : Symbol(input, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 10, 27))
35+
36+
[K in keyof T]: {
37+
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 12, 5))
38+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 10, 24))
39+
40+
needs: AllowedNeeds<T[K]>
41+
>needs : Symbol(needs, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 12, 21))
42+
>AllowedNeeds : Symbol(AllowedNeeds, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 4, 1))
43+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 10, 24))
44+
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 12, 5))
45+
46+
compute: (x: Pick<User, keyof T[K] & keyof User>) => any;
47+
>compute : Symbol(compute, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 13, 31))
48+
>x : Symbol(x, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 14, 16))
49+
>Pick : Symbol(Pick, Decl(lib.es5.d.ts, --, --))
50+
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 0, 0))
51+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 10, 24))
52+
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 12, 5))
53+
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 0, 0))
54+
55+
};
56+
}
57+
): T
58+
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 10, 24))
59+
60+
const inferred1 = extend({
61+
>inferred1 : Symbol(inferred1, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 19, 5))
62+
>extend : Symbol(extend, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 8, 2))
63+
64+
fullName: {
65+
>fullName : Symbol(fullName, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 19, 26))
66+
67+
needs: {
68+
>needs : Symbol(needs, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 20, 13))
69+
70+
name: true,
71+
>name : Symbol(name, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 21, 12))
72+
73+
last: true,
74+
>last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 22, 17))
75+
76+
},
77+
compute: (user) => `${user.name} ${user.last}`,
78+
>compute : Symbol(compute, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 24, 6))
79+
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 25, 14))
80+
>user.name : Symbol(name, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 0, 20))
81+
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 25, 14))
82+
>name : Symbol(name, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 0, 20))
83+
>user.last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 1, 22))
84+
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 25, 14))
85+
>last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 1, 22))
86+
87+
},
88+
});
89+
90+
const inferred2 = extend({
91+
>inferred2 : Symbol(inferred2, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 29, 5))
92+
>extend : Symbol(extend, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 8, 2))
93+
94+
fullName: {
95+
>fullName : Symbol(fullName, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 29, 26))
96+
97+
needs: {
98+
>needs : Symbol(needs, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 30, 13))
99+
100+
last: true,
101+
>last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 31, 12))
102+
103+
doesntExist: true // error
104+
>doesntExist : Symbol(doesntExist, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 32, 17))
105+
106+
},
107+
compute: (user) => {},
108+
>compute : Symbol(compute, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 34, 6))
109+
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType.ts, 35, 14))
110+
111+
},
112+
});
113+
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
=== tests/cases/compiler/reverseMappedTypeInferFromFilteringNameType.ts ===
2+
declare class User {
3+
>User : User
4+
5+
public name: string;
6+
>name : string
7+
8+
public last: string;
9+
>last : string
10+
11+
public age: number;
12+
>age : number
13+
}
14+
15+
type AllowedNeeds<T> = {
16+
>AllowedNeeds : AllowedNeeds<T>
17+
18+
[K in keyof T as K & keyof User]: T[K];
19+
};
20+
21+
declare function extend<T>(
22+
>extend : <T>(input: { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }) => T
23+
24+
input: {
25+
>input : { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }
26+
27+
[K in keyof T]: {
28+
needs: AllowedNeeds<T[K]>
29+
>needs : AllowedNeeds<T[K]>
30+
31+
compute: (x: Pick<User, keyof T[K] & keyof User>) => any;
32+
>compute : (x: Pick<User, keyof T[K] & keyof User>) => any
33+
>x : Pick<User, keyof T[K] & keyof User>
34+
35+
};
36+
}
37+
): T
38+
39+
const inferred1 = extend({
40+
>inferred1 : { fullName: { name: boolean; last: boolean; }; }
41+
>extend({ fullName: { needs: { name: true, last: true, }, compute: (user) => `${user.name} ${user.last}`, },}) : { fullName: { name: boolean; last: boolean; }; }
42+
>extend : <T>(input: { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }) => T
43+
>{ fullName: { needs: { name: true, last: true, }, compute: (user) => `${user.name} ${user.last}`, },} : { fullName: { needs: { name: true; last: true; }; compute: (user: Pick<User, "name" | "last">) => string; }; }
44+
45+
fullName: {
46+
>fullName : { needs: { name: true; last: true; }; compute: (user: Pick<User, "name" | "last">) => string; }
47+
>{ needs: { name: true, last: true, }, compute: (user) => `${user.name} ${user.last}`, } : { needs: { name: true; last: true; }; compute: (user: Pick<User, "name" | "last">) => string; }
48+
49+
needs: {
50+
>needs : { name: true; last: true; }
51+
>{ name: true, last: true, } : { name: true; last: true; }
52+
53+
name: true,
54+
>name : true
55+
>true : true
56+
57+
last: true,
58+
>last : true
59+
>true : true
60+
61+
},
62+
compute: (user) => `${user.name} ${user.last}`,
63+
>compute : (user: Pick<User, "name" | "last">) => string
64+
>(user) => `${user.name} ${user.last}` : (user: Pick<User, "name" | "last">) => string
65+
>user : Pick<User, "name" | "last">
66+
>`${user.name} ${user.last}` : string
67+
>user.name : string
68+
>user : Pick<User, "name" | "last">
69+
>name : string
70+
>user.last : string
71+
>user : Pick<User, "name" | "last">
72+
>last : string
73+
74+
},
75+
});
76+
77+
const inferred2 = extend({
78+
>inferred2 : { fullName: { last: boolean; doesntExist: boolean; }; }
79+
>extend({ fullName: { needs: { last: true, doesntExist: true // error }, compute: (user) => {}, },}) : { fullName: { last: boolean; doesntExist: boolean; }; }
80+
>extend : <T>(input: { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }) => T
81+
>{ fullName: { needs: { last: true, doesntExist: true // error }, compute: (user) => {}, },} : { fullName: { needs: { last: true; doesntExist: boolean; }; compute: (user: Pick<User, "last">) => void; }; }
82+
83+
fullName: {
84+
>fullName : { needs: { last: true; doesntExist: boolean; }; compute: (user: Pick<User, "last">) => void; }
85+
>{ needs: { last: true, doesntExist: true // error }, compute: (user) => {}, } : { needs: { last: true; doesntExist: boolean; }; compute: (user: Pick<User, "last">) => void; }
86+
87+
needs: {
88+
>needs : { last: true; doesntExist: boolean; }
89+
>{ last: true, doesntExist: true // error } : { last: true; doesntExist: boolean; }
90+
91+
last: true,
92+
>last : true
93+
>true : true
94+
95+
doesntExist: true // error
96+
>doesntExist : boolean
97+
>true : true
98+
99+
},
100+
compute: (user) => {},
101+
>compute : (user: Pick<User, "last">) => void
102+
>(user) => {} : (user: Pick<User, "last">) => void
103+
>user : Pick<User, "last">
104+
105+
},
106+
});
107+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @strict: true
2+
// @noEmit: true
3+
4+
declare class User {
5+
public name: string;
6+
public last: string;
7+
public age: number;
8+
}
9+
10+
type AllowedNeeds<T> = {
11+
[K in keyof T as K & keyof User]: T[K];
12+
};
13+
14+
declare function extend<T>(
15+
input: {
16+
[K in keyof T]: {
17+
needs: AllowedNeeds<T[K]>
18+
compute: (x: Pick<User, keyof T[K] & keyof User>) => any;
19+
};
20+
}
21+
): T
22+
23+
const inferred1 = extend({
24+
fullName: {
25+
needs: {
26+
name: true,
27+
last: true,
28+
},
29+
compute: (user) => `${user.name} ${user.last}`,
30+
},
31+
});
32+
33+
const inferred2 = extend({
34+
fullName: {
35+
needs: {
36+
last: true,
37+
doesntExist: true // error
38+
},
39+
compute: (user) => {},
40+
},
41+
});

0 commit comments

Comments
 (0)