-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Fixes reverse mapped type members limiting constraint #56911
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2038,6 +2038,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
| var unknownEmptyObjectType = createAnonymousType(/*symbol*/ undefined, emptySymbols, emptyArray, emptyArray, emptyArray); | ||
| var unknownUnionType = strictNullChecks ? getUnionType([undefinedType, nullType, unknownEmptyObjectType]) : unknownType; | ||
|
|
||
| var keyofConstraintObjectType = createAnonymousType(/*symbol*/ undefined, emptySymbols, emptyArray, emptyArray, [stringType, numberType, esSymbolType].map(t => createIndexInfo(t, unknownType, /*isReadonly*/ false))); // { [k: string | number | symbol]: unknown; } | ||
|
|
||
| var emptyGenericType = createAnonymousType(/*symbol*/ undefined, emptySymbols, emptyArray, emptyArray, emptyArray) as ObjectType as GenericType; | ||
| emptyGenericType.instantiations = new Map<string, TypeReference>(); | ||
|
|
||
|
|
@@ -13667,21 +13669,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
| return instantiateType(instantiable, createTypeMapper([type.indexType, type.objectType], [getNumberLiteralType(0), createTupleType([replacement])])); | ||
| } | ||
|
|
||
| // If the original mapped type had an intersection constraint we extract its components, | ||
| // and we make an attempt to do so even if the intersection has been reduced to a union. | ||
| // This entire process allows us to possibly retrieve the filtering type literals. | ||
| // e.g. { [K in keyof U & ("a" | "b") ] } -> "a" | "b" | ||
| function getLimitedConstraint(type: ReverseMappedType) { | ||
| // If the original mapped type had an union/intersection constraint | ||
| // there is a chance that it includes an intersection that could limit what members are allowed | ||
| function getReverseMappedTypeMembersLimitingConstraint(type: ReverseMappedType) { | ||
| const constraint = getConstraintTypeFromMappedType(type.mappedType); | ||
| if (!(constraint.flags & TypeFlags.Union || constraint.flags & TypeFlags.Intersection)) { | ||
| return; | ||
| } | ||
| const origin = (constraint.flags & TypeFlags.Union) ? (constraint as UnionType).origin : (constraint as IntersectionType); | ||
| if (!origin || !(origin.flags & TypeFlags.Intersection)) { | ||
| if (constraint === type.constraintType) { | ||
| return; | ||
| } | ||
| const limitedConstraint = getIntersectionType((origin as IntersectionType).types.filter(t => t !== type.constraintType)); | ||
| return limitedConstraint !== neverType ? limitedConstraint : undefined; | ||
| const mapper = appendTypeMapping(type.mappedType.mapper, type.constraintType.type, getIntersectionType([type.constraintType.type, keyofConstraintObjectType])); | ||
| return getBaseConstraintOrType(instantiateType(constraint, mapper)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a fan of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Diff without git diffDissecting one of the failures: When creating this limiting constraint for Thinking about it now - what we want is to do the same thing for all reverse mapped types created based on the So I'm not sure how to properly deal with this here - since even keeping tabs on all potential reverse mapped types created from It works right now, in a sense that we get EPC on I think that perhaps it becomes even more crucial to make those reversed mapped types aware of the original inference context etc. To get the best results we should have access to the inferred types of other type params involved in all of this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
To complete this with a test case: |
||
| } | ||
|
|
||
| function resolveReverseMappedTypeMembers(type: ReverseMappedType) { | ||
|
|
@@ -13691,14 +13687,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
| const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional; | ||
| const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : emptyArray; | ||
| const members = createSymbolTable(); | ||
| const limitedConstraint = getLimitedConstraint(type); | ||
| const membersLimitingConstraint = getReverseMappedTypeMembersLimitingConstraint(type); | ||
| 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, | ||
| // because the extra properties wouldn't get through the application of the mapped type anyway | ||
| if (limitedConstraint) { | ||
| // we skip those properties that are not assignable to the limiting constraint | ||
| // the extra properties wouldn't get through the application of the mapped type anyway | ||
| // and their inferred type might not satisfy the type parameter's constraint | ||
| // which, in turn, could fail the check if the inferred type is assignable to its constraint | ||
| // | ||
| // inferring `{ a: number; b: string }` wouldn't satisfy T's constraint so b has to be skipped here | ||
| // | ||
| // declare function fn<T extends Record<string, number>>(arg: { [K in keyof T & "a"]: T[K] }): T | ||
| // const obj = { a: 1, b: '2' }; | ||
| // fn(obj); | ||
| if (membersLimitingConstraint) { | ||
| const propertyNameType = getLiteralTypeFromProperty(prop, TypeFlags.StringOrNumberLiteralOrUnique); | ||
| if (!isTypeAssignableTo(propertyNameType, limitedConstraint)) { | ||
| if (!isTypeAssignableTo(propertyNameType, membersLimitingConstraint)) { | ||
| continue; | ||
| } | ||
| } | ||
|
|
@@ -25749,9 +25752,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
| } | ||
|
|
||
| function inferToMappedType(source: Type, target: MappedType, constraintType: Type): boolean { | ||
| if ((constraintType.flags & TypeFlags.Union) || (constraintType.flags & TypeFlags.Intersection)) { | ||
| if (constraintType.flags & TypeFlags.UnionOrIntersection) { | ||
| let result = false; | ||
| for (const type of (constraintType as (UnionType | IntersectionType)).types) { | ||
| for (const type of (constraintType as UnionOrIntersectionType).types) { | ||
| result = inferToMappedType(source, target, type) || result; | ||
| } | ||
| return result; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weswigham would you mind taking a look at this PR? It's a fix to the recently-ish merged #55811
I have a concern that using
keyofConstraintObjectTypehere is not that great since it might change the meaning ofT[K]. In reality, I'd like to just mapkeyof T... but type mappings only work on type parameters.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, swapping
keyof Whateverout forstring | number | symboland getting the resulting type is the goal here, right? That definitely aughta result in the useful bound. It's definitely possibleT[K]if it were used in a constraint could return weird stuff here by doing an instantiation in this way (which could be actually done because of some conditional type weirdness).... hmmm... Maybe instantiate withTwithT & keyofConstraintObjectTypeso specific-key indexing still turns up the specific member types, butkeyofreturnsstring | number | symbol.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes! great idea with that intersection :) I pushed out this change