Only call getLowerBoundOfKeyType on non-generic mapped types (#56280)

This commit is contained in:
Anders Hejlsberg 2023-11-20 17:07:07 -08:00 committed by GitHub
parent 9302332481
commit 3e094edc97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 62 additions and 18 deletions

View File

@ -17493,30 +17493,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return constraintType;
}
const keyTypes: Type[] = [];
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
// We have a { [P in keyof T]: X }
// `getApparentType` on the T in a generic mapped type can trigger a circularity
// (conditionals and `infer` types create a circular dependency in the constraint resolution)
// so we only eagerly manifest the keys if the constraint is nongeneric
if (!isGenericIndexType(constraintType)) {
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, TypeFlags.StringOrNumberLiteralOrUnique, !!(indexFlags & IndexFlags.StringsOnly), addMemberForKeyType);
}
else {
// we have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer the whole `keyof whatever` for later
// since it's not safe to resolve the shape of modifier type
// Calling getApparentType on the `T` of a `keyof T` in the constraint type of a generic mapped type can
// trigger a circularity. For example, `T extends { [P in keyof T & string as Captitalize<P>]: any }` is
// a circular definition. For this reason, we only eagerly manifest the keys if the constraint is non-generic.
if (isGenericIndexType(constraintType)) {
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
// We have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer
// the whole `keyof whatever` for later since it's not safe to resolve the shape of modifier type.
return getIndexTypeForGenericType(type, indexFlags);
}
// Include the generic component in the resulting type.
forEachType(constraintType, addMemberForKeyType);
}
else if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
forEachMappedTypePropertyKeyTypeAndIndexSignatureKeyType(modifiersType, TypeFlags.StringOrNumberLiteralOrUnique, !!(indexFlags & IndexFlags.StringsOnly), addMemberForKeyType);
}
else {
forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType);
}
if (isGenericIndexType(constraintType)) { // include the generic component in the resulting type
forEachType(constraintType, addMemberForKeyType);
}
// we had to pick apart the constraintType to potentially map/filter it - compare the final resulting list with the original constraintType,
// so we can return the union that preserves aliases/origin data if possible
// We had to pick apart the constraintType to potentially map/filter it - compare the final resulting list with the
// original constraintType, so we can return the union that preserves aliases/origin data if possible.
const result = indexFlags & IndexFlags.NoIndexSignatures ? filterType(getUnionType(keyTypes), t => !(t.flags & (TypeFlags.Any | TypeFlags.String))) : getUnionType(keyTypes);
if (result.flags & TypeFlags.Union && constraintType.flags & TypeFlags.Union && getTypeListId((result as UnionType).types) === getTypeListId((constraintType as UnionType).types)) {
return constraintType;

View File

@ -0,0 +1,23 @@
//// [tests/cases/compiler/circularMappedTypeConstraint.ts] ////
=== circularMappedTypeConstraint.ts ===
// Repro from #56232
declare function foo2<T extends { [P in keyof T & string as Capitalize<P>]: V }, V extends string>(a: T): T;
>foo2 : Symbol(foo2, Decl(circularMappedTypeConstraint.ts, 0, 0))
>T : Symbol(T, Decl(circularMappedTypeConstraint.ts, 2, 22))
>P : Symbol(P, Decl(circularMappedTypeConstraint.ts, 2, 35))
>T : Symbol(T, Decl(circularMappedTypeConstraint.ts, 2, 22))
>Capitalize : Symbol(Capitalize, Decl(lib.es5.d.ts, --, --))
>P : Symbol(P, Decl(circularMappedTypeConstraint.ts, 2, 35))
>V : Symbol(V, Decl(circularMappedTypeConstraint.ts, 2, 80))
>V : Symbol(V, Decl(circularMappedTypeConstraint.ts, 2, 80))
>a : Symbol(a, Decl(circularMappedTypeConstraint.ts, 2, 99))
>T : Symbol(T, Decl(circularMappedTypeConstraint.ts, 2, 22))
>T : Symbol(T, Decl(circularMappedTypeConstraint.ts, 2, 22))
export const r2 = foo2({A: "a"});
>r2 : Symbol(r2, Decl(circularMappedTypeConstraint.ts, 3, 12))
>foo2 : Symbol(foo2, Decl(circularMappedTypeConstraint.ts, 0, 0))
>A : Symbol(A, Decl(circularMappedTypeConstraint.ts, 3, 24))

View File

@ -0,0 +1,17 @@
//// [tests/cases/compiler/circularMappedTypeConstraint.ts] ////
=== circularMappedTypeConstraint.ts ===
// Repro from #56232
declare function foo2<T extends { [P in keyof T & string as Capitalize<P>]: V }, V extends string>(a: T): T;
>foo2 : <T extends { [P in keyof T & string as Capitalize<P>]: V; }, V extends string>(a: T) => T
>a : T
export const r2 = foo2({A: "a"});
>r2 : { A: string; }
>foo2({A: "a"}) : { A: string; }
>foo2 : <T extends { [P in keyof T & string as Capitalize<P>]: V; }, V extends string>(a: T) => T
>{A: "a"} : { A: string; }
>A : string
>"a" : "a"

View File

@ -0,0 +1,7 @@
// @strict: true
// @noEmit: true
// Repro from #56232
declare function foo2<T extends { [P in keyof T & string as Capitalize<P>]: V }, V extends string>(a: T): T;
export const r2 = foo2({A: "a"});