From 3e094edc976ff62bd732ace5158609caa2812aad Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 20 Nov 2023 17:07:07 -0800 Subject: [PATCH] Only call `getLowerBoundOfKeyType` on non-generic mapped types (#56280) --- src/compiler/checker.ts | 33 +++++++++---------- .../circularMappedTypeConstraint.symbols | 23 +++++++++++++ .../circularMappedTypeConstraint.types | 17 ++++++++++ .../compiler/circularMappedTypeConstraint.ts | 7 ++++ 4 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 tests/baselines/reference/circularMappedTypeConstraint.symbols create mode 100644 tests/baselines/reference/circularMappedTypeConstraint.types create mode 100644 tests/cases/compiler/circularMappedTypeConstraint.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 63255188172..b3b51787cc1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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

]: 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; diff --git a/tests/baselines/reference/circularMappedTypeConstraint.symbols b/tests/baselines/reference/circularMappedTypeConstraint.symbols new file mode 100644 index 00000000000..8c963abf1fd --- /dev/null +++ b/tests/baselines/reference/circularMappedTypeConstraint.symbols @@ -0,0 +1,23 @@ +//// [tests/cases/compiler/circularMappedTypeConstraint.ts] //// + +=== circularMappedTypeConstraint.ts === +// Repro from #56232 + +declare function foo2]: 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)) + diff --git a/tests/baselines/reference/circularMappedTypeConstraint.types b/tests/baselines/reference/circularMappedTypeConstraint.types new file mode 100644 index 00000000000..5e00e8e2165 --- /dev/null +++ b/tests/baselines/reference/circularMappedTypeConstraint.types @@ -0,0 +1,17 @@ +//// [tests/cases/compiler/circularMappedTypeConstraint.ts] //// + +=== circularMappedTypeConstraint.ts === +// Repro from #56232 + +declare function foo2]: V }, V extends string>(a: T): T; +>foo2 : ]: V; }, V extends string>(a: T) => T +>a : T + +export const r2 = foo2({A: "a"}); +>r2 : { A: string; } +>foo2({A: "a"}) : { A: string; } +>foo2 : ]: V; }, V extends string>(a: T) => T +>{A: "a"} : { A: string; } +>A : string +>"a" : "a" + diff --git a/tests/cases/compiler/circularMappedTypeConstraint.ts b/tests/cases/compiler/circularMappedTypeConstraint.ts new file mode 100644 index 00000000000..90160560d32 --- /dev/null +++ b/tests/cases/compiler/circularMappedTypeConstraint.ts @@ -0,0 +1,7 @@ +// @strict: true +// @noEmit: true + +// Repro from #56232 + +declare function foo2]: V }, V extends string>(a: T): T; +export const r2 = foo2({A: "a"});