Skip to content
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

Infer from filtering mapped types #52972

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
10a9bb6
Infer from filtering mapped types
Andarist Feb 25, 2023
22fec49
Merge remote-tracking branch 'origin/main' into infer-from-filterin-m…
Andarist Mar 19, 2023
e8676f2
Merge remote-tracking branch 'origin/main' into infer-from-filterin-m…
Andarist Jun 13, 2023
fe90e8a
Merge remote-tracking branch 'origin/main' into infer-from-filterin-m…
Andarist Sep 20, 2023
17618ea
Merge remote-tracking branch 'origin/main' into infer-from-filterin-m…
Andarist Dec 1, 2023
cb96fc1
Add more tests
Andarist Dec 1, 2023
3a44904
Limit inferred keys based on `nameType` too
Andarist Dec 30, 2023
55bb333
Merge remote-tracking branch 'origin/main' into infer-from-filterin-m…
Andarist Dec 30, 2023
9dfeab2
fix format
Andarist Dec 30, 2023
602d628
grab the constraint of limited constraint
Andarist Dec 30, 2023
d1f9006
fix the get base constraint call
Andarist Dec 31, 2023
1271786
add extra test cases to showcase how constraint limiting is important…
Andarist Dec 31, 2023
6c74957
showcase additional fix with mixed union and intersection
Andarist Dec 31, 2023
998b96b
add tests for combined intersection limiting and name type limiting
Andarist Dec 31, 2023
171ee93
improve comments and variable names
Andarist Dec 31, 2023
5f3cf18
replace record with object
Andarist Dec 31, 2023
59b6549
fix format
Andarist Dec 31, 2023
37521a7
Remove what got carved out to a separate bug fixing PR
Andarist Dec 31, 2023
fde40d5
Merge remote-tracking branch 'origin/main' into infer-from-filterin-m…
Andarist Nov 26, 2024
9deef9d
Merge remote-tracking branch 'origin/main' into infer-from-filterin-m…
Andarist Jan 11, 2025
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
28 changes: 20 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14071,15 +14071,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType) || unknownType, readonlyMask && indexInfo.isReadonly)] : emptyArray;
const members = createSymbolTable();
const limitedConstraint = getLimitedConstraint(type);
const nameType = getNameTypeFromMappedType(type.mappedType);

for (const prop of getPropertiesOfType(type.source)) {
// In case of a reverse mapped type with an intersection constraint, if we were able to
// extract the filtering type literals we skip those properties that are not assignable to them,
// In case of a reverse mapped type with an intersection constraint or a name type
// we skip those properties that are not assignable to them
// because the extra properties wouldn't get through the application of the mapped type anyway
if (limitedConstraint) {
if (limitedConstraint || nameType) {
const propertyNameType = getLiteralTypeFromProperty(prop, TypeFlags.StringOrNumberLiteralOrUnique);
if (!isTypeAssignableTo(propertyNameType, limitedConstraint)) {
if (limitedConstraint && !isTypeAssignableTo(propertyNameType, limitedConstraint)) {
continue;
}
if (nameType) {
const nameMapper = appendTypeMapping(type.mappedType.mapper, getTypeParameterFromMappedType(type.mappedType), propertyNameType);
const instantiatedNameType = instantiateType(nameType, nameMapper);
if (instantiatedNameType.flags & TypeFlags.Never) {
continue;
}
}
}
const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0);
const inferredProp = createSymbol(SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ReverseMappedSymbol;
Expand Down Expand Up @@ -26713,10 +26722,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (isGenericMappedType(source) && isGenericMappedType(target)) {
inferFromGenericMappedTypes(source, target);
}
if (getObjectFlags(target) & ObjectFlags.Mapped && !(target as MappedType).declaration.nameType) {
const constraintType = getConstraintTypeFromMappedType(target as MappedType);
if (inferToMappedType(source, target as MappedType, constraintType)) {
return;
if (getObjectFlags(target) & ObjectFlags.Mapped) {
const mappedType = target as MappedType;
if (getMappedTypeNameTypeKind(mappedType) !== MappedTypeNameTypeKind.Remapping) {
const constraintType = getConstraintTypeFromMappedType(mappedType);
if (inferToMappedType(source, mappedType, constraintType)) {
return;
}
}
}
// Infer from the members of source and target only if the two types are possibly related
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
reverseMappedTypeInferFromFilteringNameType1.ts(34,7): error TS2353: Object literal may only specify known properties, and 'doesntExist' does not exist in type 'AllowedNeeds<{ last: boolean; }>'.


==== reverseMappedTypeInferFromFilteringNameType1.ts (1 errors) ====
declare class User {
public name: string;
public last: string;
public age: number;
}

type AllowedNeeds<T> = {
[K in keyof T as K & keyof User]: T[K];
};

declare function extend<T>(
input: {
[K in keyof T]: {
needs: AllowedNeeds<T[K]>
compute: (x: Pick<User, keyof T[K] & keyof User>) => any;
};
}
): T

const inferred1 = extend({
fullName: {
needs: {
name: true,
last: true,
},
compute: (user) => `${user.name} ${user.last}`,
},
});

const inferred2 = extend({
fullName: {
needs: {
last: true,
doesntExist: true // error
~~~~~~~~~~~
!!! error TS2353: Object literal may only specify known properties, and 'doesntExist' does not exist in type 'AllowedNeeds<{ last: boolean; }>'.
!!! related TS6500 reverseMappedTypeInferFromFilteringNameType1.ts:14:7: The expected type comes from property 'needs' which is declared here on type '{ needs: AllowedNeeds<{ last: boolean; }>; compute: (x: Pick<User, "last">) => any; }'
},
compute: (user) => {},
},
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//// [tests/cases/compiler/reverseMappedTypeInferFromFilteringNameType1.ts] ////

=== reverseMappedTypeInferFromFilteringNameType1.ts ===
declare class User {
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 0, 0))

public name: string;
>name : Symbol(User.name, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 0, 20))

public last: string;
>last : Symbol(User.last, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 1, 22))

public age: number;
>age : Symbol(User.age, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 2, 22))
}

type AllowedNeeds<T> = {
>AllowedNeeds : Symbol(AllowedNeeds, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 4, 1))
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 6, 18))

[K in keyof T as K & keyof User]: T[K];
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 7, 3))
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 6, 18))
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 7, 3))
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 0, 0))
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 6, 18))
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 7, 3))

};

declare function extend<T>(
>extend : Symbol(extend, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 8, 2))
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 10, 24))

input: {
>input : Symbol(input, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 10, 27))

[K in keyof T]: {
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 12, 5))
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 10, 24))

needs: AllowedNeeds<T[K]>
>needs : Symbol(needs, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 12, 21))
>AllowedNeeds : Symbol(AllowedNeeds, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 4, 1))
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 10, 24))
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 12, 5))

compute: (x: Pick<User, keyof T[K] & keyof User>) => any;
>compute : Symbol(compute, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 13, 31))
>x : Symbol(x, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 14, 16))
>Pick : Symbol(Pick, Decl(lib.es5.d.ts, --, --))
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 0, 0))
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 10, 24))
>K : Symbol(K, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 12, 5))
>User : Symbol(User, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 0, 0))

};
}
): T
>T : Symbol(T, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 10, 24))

const inferred1 = extend({
>inferred1 : Symbol(inferred1, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 19, 5))
>extend : Symbol(extend, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 8, 2))

fullName: {
>fullName : Symbol(fullName, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 19, 26))

needs: {
>needs : Symbol(needs, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 20, 13))

name: true,
>name : Symbol(name, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 21, 12))

last: true,
>last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 22, 17))

},
compute: (user) => `${user.name} ${user.last}`,
>compute : Symbol(compute, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 24, 6))
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 25, 14))
>user.name : Symbol(name, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 0, 20))
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 25, 14))
>name : Symbol(name, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 0, 20))
>user.last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 1, 22))
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 25, 14))
>last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 1, 22))

},
});

const inferred2 = extend({
>inferred2 : Symbol(inferred2, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 29, 5))
>extend : Symbol(extend, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 8, 2))

fullName: {
>fullName : Symbol(fullName, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 29, 26))

needs: {
>needs : Symbol(needs, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 30, 13))

last: true,
>last : Symbol(last, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 31, 12))

doesntExist: true // error
>doesntExist : Symbol(doesntExist, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 32, 17))

},
compute: (user) => {},
>compute : Symbol(compute, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 34, 6))
>user : Symbol(user, Decl(reverseMappedTypeInferFromFilteringNameType1.ts, 35, 14))

},
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//// [tests/cases/compiler/reverseMappedTypeInferFromFilteringNameType1.ts] ////

=== reverseMappedTypeInferFromFilteringNameType1.ts ===
declare class User {
>User : User
> : ^^^^

public name: string;
>name : string
> : ^^^^^^

public last: string;
>last : string
> : ^^^^^^

public age: number;
>age : number
> : ^^^^^^
}

type AllowedNeeds<T> = {
>AllowedNeeds : AllowedNeeds<T>
> : ^^^^^^^^^^^^^^^

[K in keyof T as K & keyof User]: T[K];
};

declare function extend<T>(
>extend : <T>(input: { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }) => T
> : ^ ^^ ^^ ^^^^^

input: {
>input : { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }
> : ^^^ ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^

[K in keyof T]: {
needs: AllowedNeeds<T[K]>
>needs : AllowedNeeds<T[K]>
> : ^^^^^^^^^^^^^^^^^^

compute: (x: Pick<User, keyof T[K] & keyof User>) => any;
>compute : (x: Pick<User, keyof T[K] & keyof User>) => any
> : ^ ^^ ^^^^^
>x : Pick<User, keyof T[K] & keyof User>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

};
}
): T

const inferred1 = extend({
>inferred1 : { fullName: { name: boolean; last: boolean; }; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>extend({ fullName: { needs: { name: true, last: true, }, compute: (user) => `${user.name} ${user.last}`, },}) : { fullName: { name: boolean; last: boolean; }; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>extend : <T>(input: { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }) => T
> : ^ ^^ ^^ ^^^^^
>{ 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; }; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

fullName: {
>fullName : { needs: { name: true; last: true; }; compute: (user: Pick<User, "name" | "last">) => string; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>{ needs: { name: true, last: true, }, compute: (user) => `${user.name} ${user.last}`, } : { needs: { name: true; last: true; }; compute: (user: Pick<User, "name" | "last">) => string; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

needs: {
>needs : { name: true; last: true; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^
>{ name: true, last: true, } : { name: true; last: true; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^

name: true,
>name : true
> : ^^^^
>true : true
> : ^^^^

last: true,
>last : true
> : ^^^^
>true : true
> : ^^^^

},
compute: (user) => `${user.name} ${user.last}`,
>compute : (user: Pick<User, "name" | "last">) => string
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>(user) => `${user.name} ${user.last}` : (user: Pick<User, "name" | "last">) => string
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>user : Pick<User, "name" | "last">
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^
>`${user.name} ${user.last}` : string
> : ^^^^^^
>user.name : string
> : ^^^^^^
>user : Pick<User, "name" | "last">
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^
>name : string
> : ^^^^^^
>user.last : string
> : ^^^^^^
>user : Pick<User, "name" | "last">
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^
>last : string
> : ^^^^^^

},
});

const inferred2 = extend({
>inferred2 : { fullName: { last: boolean; }; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>extend({ fullName: { needs: { last: true, doesntExist: true // error }, compute: (user) => {}, },}) : { fullName: { last: boolean; }; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>extend : <T>(input: { [K in keyof T]: { needs: AllowedNeeds<T[K]>; compute: (x: Pick<User, keyof T[K] & keyof User>) => any; }; }) => T
> : ^ ^^ ^^ ^^^^^
>{ fullName: { needs: { last: true, doesntExist: true // error }, compute: (user) => {}, },} : { fullName: { needs: { last: true; doesntExist: boolean; }; compute: (user: Pick<User, "last">) => void; }; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

fullName: {
>fullName : { needs: { last: true; doesntExist: boolean; }; compute: (user: Pick<User, "last">) => void; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>{ needs: { last: true, doesntExist: true // error }, compute: (user) => {}, } : { needs: { last: true; doesntExist: boolean; }; compute: (user: Pick<User, "last">) => void; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

needs: {
>needs : { last: true; doesntExist: boolean; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>{ last: true, doesntExist: true // error } : { last: true; doesntExist: boolean; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

last: true,
>last : true
> : ^^^^
>true : true
> : ^^^^

doesntExist: true // error
>doesntExist : boolean
> : ^^^^^^^
>true : true
> : ^^^^

},
compute: (user) => {},
>compute : (user: Pick<User, "last">) => void
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>(user) => {} : (user: Pick<User, "last">) => void
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>user : Pick<User, "last">
> : ^^^^^^^^^^^^^^^^^^

},
});

Loading
Loading