Skip to content

Commit 03848cf

Browse files
committed
Fixes reverse mapped type members limiting constraint
1 parent f2e9ebd commit 03848cf

12 files changed

+781
-843
lines changed

src/compiler/checker.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
20382038
var unknownEmptyObjectType = createAnonymousType(/*symbol*/ undefined, emptySymbols, emptyArray, emptyArray, emptyArray);
20392039
var unknownUnionType = strictNullChecks ? getUnionType([undefinedType, nullType, unknownEmptyObjectType]) : unknownType;
20402040

2041+
var keyofConstraintObjectType = createAnonymousType(/*symbol*/ undefined, emptySymbols, emptyArray, emptyArray, [stringType, numberType, esSymbolType].map(t => createIndexInfo(t, unknownType, /*isReadonly*/ false))); // { [k: string | number | symbol]: unknown; }
2042+
20412043
var emptyGenericType = createAnonymousType(/*symbol*/ undefined, emptySymbols, emptyArray, emptyArray, emptyArray) as ObjectType as GenericType;
20422044
emptyGenericType.instantiations = new Map<string, TypeReference>();
20432045

@@ -13667,21 +13669,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1366713669
return instantiateType(instantiable, createTypeMapper([type.indexType, type.objectType], [getNumberLiteralType(0), createTupleType([replacement])]));
1366813670
}
1366913671

13670-
// If the original mapped type had an intersection constraint we extract its components,
13671-
// and we make an attempt to do so even if the intersection has been reduced to a union.
13672-
// This entire process allows us to possibly retrieve the filtering type literals.
13673-
// e.g. { [K in keyof U & ("a" | "b") ] } -> "a" | "b"
13674-
function getLimitedConstraint(type: ReverseMappedType) {
13672+
// If the original mapped type had an union/intersection constraint
13673+
// there is a chance that it includes an intersection that could limit what members are allowed
13674+
function getReverseMappedTypeMembersLimitingConstraint(type: ReverseMappedType) {
1367513675
const constraint = getConstraintTypeFromMappedType(type.mappedType);
13676-
if (!(constraint.flags & TypeFlags.Union || constraint.flags & TypeFlags.Intersection)) {
13677-
return;
13678-
}
13679-
const origin = (constraint.flags & TypeFlags.Union) ? (constraint as UnionType).origin : (constraint as IntersectionType);
13680-
if (!origin || !(origin.flags & TypeFlags.Intersection)) {
13676+
if (constraint === type.constraintType) {
1368113677
return;
1368213678
}
13683-
const limitedConstraint = getIntersectionType((origin as IntersectionType).types.filter(t => t !== type.constraintType));
13684-
return limitedConstraint !== neverType ? limitedConstraint : undefined;
13679+
const mapper = appendTypeMapping(type.mappedType.mapper, type.constraintType.type, keyofConstraintObjectType);
13680+
return getBaseConstraintOrType(instantiateType(constraint, mapper));
1368513681
}
1368613682

1368713683
function resolveReverseMappedTypeMembers(type: ReverseMappedType) {
@@ -13691,14 +13687,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1369113687
const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional;
1369213688
const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : emptyArray;
1369313689
const members = createSymbolTable();
13694-
const limitedConstraint = getLimitedConstraint(type);
13690+
const membersLimitingConstraint = getReverseMappedTypeMembersLimitingConstraint(type);
1369513691
for (const prop of getPropertiesOfType(type.source)) {
13696-
// In case of a reverse mapped type with an intersection constraint, if we were able to
13697-
// extract the filtering type literals we skip those properties that are not assignable to them,
13698-
// because the extra properties wouldn't get through the application of the mapped type anyway
13699-
if (limitedConstraint) {
13692+
// we skip those properties that are not assignable to the limiting constraint
13693+
// the extra properties wouldn't get through the application of the mapped type anyway
13694+
// and their inferred type might not satisfy the type parameter's constraint
13695+
// which, in turn, could fail the check if the inferred type is assignable to its constraint
13696+
//
13697+
// inferring `{ a: number; b: string }` wouldn't satisfy T's constraint so b has to be skipped here
13698+
//
13699+
// declare function fn<T extends Record<string, number>>(arg: { [K in keyof T & "a"]: T[K] }): T
13700+
// const obj = { a: 1, b: '2' };
13701+
// fn(obj);
13702+
if (membersLimitingConstraint) {
1370013703
const propertyNameType = getLiteralTypeFromProperty(prop, TypeFlags.StringOrNumberLiteralOrUnique);
13701-
if (!isTypeAssignableTo(propertyNameType, limitedConstraint)) {
13704+
if (!isTypeAssignableTo(propertyNameType, membersLimitingConstraint)) {
1370213705
continue;
1370313706
}
1370413707
}
@@ -25749,9 +25752,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2574925752
}
2575025753

2575125754
function inferToMappedType(source: Type, target: MappedType, constraintType: Type): boolean {
25752-
if ((constraintType.flags & TypeFlags.Union) || (constraintType.flags & TypeFlags.Intersection)) {
25755+
if (constraintType.flags & TypeFlags.UnionOrIntersection) {
2575325756
let result = false;
25754-
for (const type of (constraintType as (UnionType | IntersectionType)).types) {
25757+
for (const type of (constraintType as UnionOrIntersectionType).types) {
2575525758
result = inferToMappedType(source, target, type) || result;
2575625759
}
2575725760
return result;

0 commit comments

Comments
 (0)