From a06f85b9cfe2a06088ec3d849710af2332b6d986 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 15 Jan 2026 21:21:47 +0000
Subject: [PATCH 1/4] Initial plan
From 8000e62ee0779414b88ccf595645a2ef98b52c67 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 15 Jan 2026 21:34:51 +0000
Subject: [PATCH 2/4] Port PR #60528: Fix crash related to index type deferral
on generic mapped types
- Add TypeFlagsIndex to Simplifiable flags
- Remove hasDistributiveNameType function
- Update shouldDeferIndexType to check for nameType instead
- Add getSimplifiedIndexType function
- Remove conditional getIndexTypeForMappedType call in checkIndexedAccessIndexType
- Update comment about key remapping in getIndexTypeForMappedType
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
---
internal/checker/checker.go | 57 ++++++++-----------------------------
internal/checker/types.go | 2 +-
2 files changed, 13 insertions(+), 46 deletions(-)
diff --git a/internal/checker/checker.go b/internal/checker/checker.go
index b167c9bf4a0..f027d77b796 100644
--- a/internal/checker/checker.go
+++ b/internal/checker/checker.go
@@ -8070,16 +8070,9 @@ func (c *Checker) checkIndexedAccessIndexType(t *Type, accessNode *ast.Node) *Ty
// Check if the index type is assignable to 'keyof T' for the object type.
objectType := t.AsIndexedAccessType().objectType
indexType := t.AsIndexedAccessType().indexType
- // skip index type deferral on remapping mapped types
- var objectIndexType *Type
- if c.isGenericMappedType(objectType) && c.getMappedTypeNameTypeKind(objectType) == MappedTypeNameTypeKindRemapping {
- objectIndexType = c.getIndexTypeForMappedType(objectType, IndexFlagsNone)
- } else {
- objectIndexType = c.getIndexTypeEx(objectType, IndexFlagsNone)
- }
hasNumberIndexInfo := c.getIndexInfoOfType(objectType, c.numberType) != nil
if everyType(indexType, func(t *Type) bool {
- return c.isTypeAssignableTo(t, objectIndexType) || hasNumberIndexInfo && c.isApplicableIndexType(t, c.numberType)
+ return c.isTypeAssignableTo(t, c.getIndexTypeEx(objectType, IndexFlagsNone)) || hasNumberIndexInfo && c.isApplicableIndexType(t, c.numberType)
}) {
if accessNode.Kind == ast.KindElementAccessExpression && ast.IsAssignmentTarget(accessNode) && objectType.objectFlags&ObjectFlagsMapped != 0 && getMappedTypeModifiers(objectType)&MappedTypeModifiersIncludeReadonly != 0 {
c.error(accessNode, diagnostics.Index_signature_in_type_0_only_permits_reading, c.TypeToString(objectType))
@@ -26063,46 +26056,11 @@ func (c *Checker) getSubstitutionIntersection(t *Type) *Type {
func (c *Checker) shouldDeferIndexType(t *Type, indexFlags IndexFlags) bool {
return t.flags&TypeFlagsInstantiableNonPrimitive != 0 ||
c.isGenericTupleType(t) ||
- c.isGenericMappedType(t) && (!c.hasDistributiveNameType(t) || c.getMappedTypeNameTypeKind(t) == MappedTypeNameTypeKindRemapping) ||
+ c.isGenericMappedType(t) && c.getNameTypeFromMappedType(t) != nil ||
t.flags&TypeFlagsUnion != 0 && indexFlags&IndexFlagsNoReducibleCheck == 0 && c.isGenericReducibleType(t) ||
t.flags&TypeFlagsIntersection != 0 && c.maybeTypeOfKind(t, TypeFlagsInstantiable) && core.Some(t.Types(), c.IsEmptyAnonymousObjectType)
}
-// Ordinarily we reduce a keyof M, where M is a mapped type { [P in K as N
]: X }, to simply N. This however presumes
-// that N distributes over union types, i.e. that N is equivalent to N | N | N. Specifically, we only
-// want to perform the reduction when the name type of a mapped type is distributive with respect to the type variable
-// introduced by the 'in' clause of the mapped type. Note that non-generic types are considered to be distributive because
-// they're the same type regardless of what's being distributed over.
-func (c *Checker) hasDistributiveNameType(mappedType *Type) bool {
- typeVariable := c.getTypeParameterFromMappedType(mappedType)
- var isDistributive func(*Type) bool
- isDistributive = func(t *Type) bool {
- switch {
- case t.flags&(TypeFlagsAnyOrUnknown|TypeFlagsPrimitive|TypeFlagsNever|TypeFlagsTypeParameter|TypeFlagsObject|TypeFlagsNonPrimitive) != 0:
- return true
- case t.flags&TypeFlagsConditional != 0:
- return t.AsConditionalType().root.isDistributive && t.AsConditionalType().checkType == typeVariable
- case t.flags&TypeFlagsUnionOrIntersection != 0:
- return core.Every(t.Types(), isDistributive)
- case t.flags&TypeFlagsTemplateLiteral != 0:
- return core.Every(t.AsTemplateLiteralType().types, isDistributive)
- case t.flags&TypeFlagsIndexedAccess != 0:
- return isDistributive(t.AsIndexedAccessType().objectType) && isDistributive(t.AsIndexedAccessType().indexType)
- case t.flags&TypeFlagsSubstitution != 0:
- return isDistributive(t.AsSubstitutionType().baseType) && isDistributive(t.AsSubstitutionType().constraint)
- case t.flags&TypeFlagsStringMapping != 0:
- return isDistributive(t.Target())
- default:
- return false
- }
- }
- nameType := c.getNameTypeFromMappedType(mappedType)
- if nameType == nil {
- nameType = typeVariable
- }
- return isDistributive(nameType)
-}
-
func (c *Checker) getMappedTypeNameTypeKind(t *Type) MappedTypeNameTypeKind {
nameType := c.getNameTypeFromMappedType(t)
if nameType == nil {
@@ -26155,7 +26113,7 @@ func (c *Checker) getIndexTypeForMappedType(t *Type, indexFlags IndexFlags) *Typ
// a circular definition. For this reason, we only eagerly manifest the keys if the constraint is non-generic.
if c.isGenericIndexType(constraintType) {
if c.isMappedTypeWithKeyofConstraintDeclaration(t) {
- // We have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer
+ // We have a generic index and a homomorphic mapping and a 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 c.getIndexTypeForGenericType(t, indexFlags)
}
@@ -27117,6 +27075,8 @@ func (c *Checker) getSimplifiedType(t *Type, writing bool) *Type {
return c.getSimplifiedIndexedAccessType(t, writing)
case t.flags&TypeFlagsConditional != 0:
return c.getSimplifiedConditionalType(t, writing)
+ case t.flags&TypeFlagsIndex != 0:
+ return c.getSimplifiedIndexType(t)
}
return t
}
@@ -27232,6 +27192,13 @@ func (c *Checker) getSimplifiedConditionalType(t *Type, writing bool) *Type {
return t
}
+func (c *Checker) getSimplifiedIndexType(t *Type) *Type {
+ if c.isGenericMappedType(t.AsIndexType().target) && c.getNameTypeFromMappedType(t.AsIndexType().target) != nil && !c.isMappedTypeWithKeyofConstraintDeclaration(t.AsIndexType().target) {
+ return c.getIndexTypeForMappedType(t.AsIndexType().target, IndexFlagsNone)
+ }
+ return t
+}
+
// Invokes union simplification logic to determine if an intersection is considered empty as a union constituent
func (c *Checker) isIntersectionEmpty(type1 *Type, type2 *Type) bool {
return c.getUnionType([]*Type{c.intersectTypes(type1, type2), c.neverType}).flags&TypeFlagsNever != 0
diff --git a/internal/checker/types.go b/internal/checker/types.go
index 3e2aafbd4c3..8900314987d 100644
--- a/internal/checker/types.go
+++ b/internal/checker/types.go
@@ -450,7 +450,7 @@ const (
TypeFlagsInstantiable = TypeFlagsInstantiableNonPrimitive | TypeFlagsInstantiablePrimitive
TypeFlagsStructuredOrInstantiable = TypeFlagsStructuredType | TypeFlagsInstantiable
TypeFlagsObjectFlagsType = TypeFlagsAny | TypeFlagsNullable | TypeFlagsNever | TypeFlagsObject | TypeFlagsUnion | TypeFlagsIntersection
- TypeFlagsSimplifiable = TypeFlagsIndexedAccess | TypeFlagsConditional
+ TypeFlagsSimplifiable = TypeFlagsIndexedAccess | TypeFlagsConditional | TypeFlagsIndex
TypeFlagsSingleton = TypeFlagsAny | TypeFlagsUnknown | TypeFlagsString | TypeFlagsNumber | TypeFlagsBoolean | TypeFlagsBigInt | TypeFlagsESSymbol | TypeFlagsVoid | TypeFlagsUndefined | TypeFlagsNull | TypeFlagsNever | TypeFlagsNonPrimitive
// 'TypeFlagsNarrowable' types are types where narrowing actually narrows.
// This *should* be every type other than null, undefined, void, and never
From b5f9ea09347fe8e7b78c1dede3feda12f72c5ca4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 15 Jan 2026 21:42:33 +0000
Subject: [PATCH 3/4] Accept baseline changes for PR #60528
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
---
.../mappedTypeConstraints2.errors.txt | 8 +++----
.../mappedTypeConstraints2.errors.txt.diff | 24 +++++++++++++++++++
.../conformance/mappedTypeConstraints2.types | 8 +++----
.../mappedTypeConstraints2.types.diff | 22 +++++++++++------
4 files changed, 47 insertions(+), 15 deletions(-)
create mode 100644 testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt.diff
diff --git a/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt
index 0d3b3244c9e..031fbe2ce9c 100644
--- a/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt
+++ b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt
@@ -4,8 +4,8 @@ mappedTypeConstraints2.ts(16,11): error TS2322: Type 'Mapped3[Uppercase]'
Type 'Mapped3[Uppercase]' is not assignable to type '{ a: K; }'.
Type 'Mapped3[string]' is not assignable to type '{ a: K; }'.
mappedTypeConstraints2.ts(42,7): error TS2322: Type 'Mapped6[keyof Mapped6]' is not assignable to type '`_${string}`'.
- Type 'Mapped6[string] | Mapped6[number] | Mapped6[symbol]' is not assignable to type '`_${string}`'.
- Type 'Mapped6[string]' is not assignable to type '`_${string}`'.
+ Type 'Mapped6[`_${K}`]' is not assignable to type '`_${string}`'.
+ Type 'Mapped6[`_${string}`]' is not assignable to type '`_${string}`'.
mappedTypeConstraints2.ts(51,57): error TS2322: Type 'Foo[`get${T}`]' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo[`get${T}`]'.
mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys[`_${K}`]' is not assignable to type 'true'.
@@ -64,8 +64,8 @@ mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys[keyof Mapped6]' is not assignable to type '`_${string}`'.
-!!! error TS2322: Type 'Mapped6[string] | Mapped6[number] | Mapped6[symbol]' is not assignable to type '`_${string}`'.
-!!! error TS2322: Type 'Mapped6[string]' is not assignable to type '`_${string}`'.
+!!! error TS2322: Type 'Mapped6[`_${K}`]' is not assignable to type '`_${string}`'.
+!!! error TS2322: Type 'Mapped6[`_${string}`]' is not assignable to type '`_${string}`'.
}
// Repro from #47794
diff --git a/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt.diff b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt.diff
new file mode 100644
index 00000000000..2bd30938f06
--- /dev/null
+++ b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.errors.txt.diff
@@ -0,0 +1,24 @@
+--- old.mappedTypeConstraints2.errors.txt
++++ new.mappedTypeConstraints2.errors.txt
+@@= skipped -3, +3 lines =@@
+ Type 'Mapped3[Uppercase]' is not assignable to type '{ a: K; }'.
+ Type 'Mapped3[string]' is not assignable to type '{ a: K; }'.
+ mappedTypeConstraints2.ts(42,7): error TS2322: Type 'Mapped6[keyof Mapped6]' is not assignable to type '`_${string}`'.
+- Type 'Mapped6[string] | Mapped6[number] | Mapped6[symbol]' is not assignable to type '`_${string}`'.
+- Type 'Mapped6[string]' is not assignable to type '`_${string}`'.
++ Type 'Mapped6[`_${K}`]' is not assignable to type '`_${string}`'.
++ Type 'Mapped6[`_${string}`]' is not assignable to type '`_${string}`'.
+ mappedTypeConstraints2.ts(51,57): error TS2322: Type 'Foo[`get${T}`]' is not assignable to type 'T'.
+ 'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo[`get${T}`]'.
+ mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys[`_${K}`]' is not assignable to type 'true'.
+@@= skipped -60, +60 lines =@@
+ let s: `_${string}` = obj[key]; // Error
+ ~
+ !!! error TS2322: Type 'Mapped6[keyof Mapped6]' is not assignable to type '`_${string}`'.
+-!!! error TS2322: Type 'Mapped6[string] | Mapped6[number] | Mapped6[symbol]' is not assignable to type '`_${string}`'.
+-!!! error TS2322: Type 'Mapped6[string]' is not assignable to type '`_${string}`'.
++!!! error TS2322: Type 'Mapped6[`_${K}`]' is not assignable to type '`_${string}`'.
++!!! error TS2322: Type 'Mapped6[`_${string}`]' is not assignable to type '`_${string}`'.
+ }
+
+ // Repro from #47794
\ No newline at end of file
diff --git a/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types
index 12fd1dc872b..44e6820acc1 100644
--- a/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types
+++ b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types
@@ -77,15 +77,15 @@ type Mapped5 = {
};
function f5(obj: Mapped5, key: keyof Mapped5) {
->f5 : (obj: Mapped5, key: K extends `_${string}` ? K : never) => void
+>f5 : (obj: Mapped5, key: keyof Mapped5) => void
>obj : Mapped5
->key : K extends `_${string}` ? K : never
+>key : keyof Mapped5
let s: `_${string}` = obj[key];
>s : `_${string}`
->obj[key] : Mapped5[K extends `_${string}` ? K : never]
+>obj[key] : Mapped5[keyof Mapped5]
>obj : Mapped5
->key : K extends `_${string}` ? K : never
+>key : keyof Mapped5
}
// repro from #53066#issuecomment-1913384757
diff --git a/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types.diff b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types.diff
index bd45a2f826e..25cf2359270 100644
--- a/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types.diff
+++ b/testdata/baselines/reference/submodule/conformance/mappedTypeConstraints2.types.diff
@@ -9,16 +9,24 @@
>obj : Mapped4
>key : K
-@@= skipped -18, +18 lines =@@
- };
-
+@@= skipped -20, +20 lines =@@
function f5(obj: Mapped5, key: keyof Mapped5) {
-->f5 : (obj: Mapped5, key: keyof Mapped5) => void
-+>f5 : (obj: Mapped5, key: K extends `_${string}` ? K : never) => void
+ >f5 : (obj: Mapped5, key: keyof Mapped5) => void
+ >obj : Mapped5
+->key : K extends `_${string}` ? K : never
++>key : keyof Mapped5
+
+ let s: `_${string}` = obj[key];
+ >s : `_${string}`
+->obj[key] : Mapped5[K extends `_${string}` ? K : never]
++>obj[key] : Mapped5[keyof Mapped5]
>obj : Mapped5
- >key : K extends `_${string}` ? K : never
+->key : K extends `_${string}` ? K : never
++>key : keyof Mapped5
+ }
-@@= skipped -74, +74 lines =@@
+ // repro from #53066#issuecomment-1913384757
+@@= skipped -72, +72 lines =@@
>key : string
>val : any
>Object.entries(obj) : [string, any][]
From 5675e7eac755b9e9c08da3c03978e1c84377d319 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 15 Jan 2026 22:25:32 +0000
Subject: [PATCH 4/4] Add regression tests for PR #60528
Add two local Go tests for the crash fixes:
- TestKeyRemappingKeyofResult2: Tests microsoft/TypeScript#56239
- TestMappedTypeAsClauseRecursiveNoCrash: Tests microsoft/TypeScript#60476
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
---
internal/checker/checker_test.go | 142 +++++++++++++++++++++++++++++++
1 file changed, 142 insertions(+)
diff --git a/internal/checker/checker_test.go b/internal/checker/checker_test.go
index d611d3b9fa4..0ad434a3cee 100644
--- a/internal/checker/checker_test.go
+++ b/internal/checker/checker_test.go
@@ -61,6 +61,148 @@ foo.bar;`
}
}
+// TestKeyRemappingKeyofResult2 tests that index types for generic mapped types with name types
+// don't crash (regression test for microsoft/TypeScript#56239)
+func TestKeyRemappingKeyofResult2(t *testing.T) {
+ t.Parallel()
+
+ content := `// https://github.com/microsoft/TypeScript/issues/56239
+
+type Values = T[keyof T];
+
+type ProvidedActor = {
+ src: string;
+ logic: unknown;
+};
+
+interface StateMachineConfig {
+ invoke: {
+ src: TActors["src"];
+ };
+}
+
+declare function setup>(_: {
+ actors: {
+ [K in keyof TActors]: TActors[K];
+ };
+}): {
+ createMachine: (
+ config: StateMachineConfig<
+ Values<{
+ [K in keyof TActors as K & string]: {
+ src: K;
+ logic: TActors[K];
+ };
+ }>
+ >,
+ ) => void;
+};`
+
+ fs := vfstest.FromMap(map[string]string{
+ "/test.ts": content,
+ "/tsconfig.json": `{
+ "compilerOptions": {
+ "strict": true,
+ "noEmit": true
+ },
+ "files": ["test.ts"]
+ }`,
+ }, false)
+ fs = bundled.WrapFS(fs)
+
+ cd := "/"
+ host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil)
+
+ parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, host, nil)
+ assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")
+
+ p := compiler.NewProgram(compiler.ProgramOptions{
+ Config: parsed,
+ Host: host,
+ })
+ p.BindSourceFiles()
+ c, done := p.GetTypeChecker(t.Context())
+ defer done()
+
+ // The test passes if we can get a type checker without crashing
+ assert.Assert(t, c != nil)
+}
+
+// TestMappedTypeAsClauseRecursiveNoCrash tests that recursive mapped types with as clauses
+// don't crash when computing keyof (regression test for microsoft/TypeScript#60476)
+func TestMappedTypeAsClauseRecursiveNoCrash(t *testing.T) {
+ t.Parallel()
+
+ content := `// https://github.com/microsoft/TypeScript/issues/60476
+
+export type FlattenType = {
+ [Key in keyof Source as Key extends string
+ ? Source[Key] extends object
+ ? ` + "`${Key}.${keyof FlattenType & string}`" + `
+ : Key
+ : never]-?: Target;
+};
+
+type FieldSelect = {
+ table: string;
+ field: string;
+};
+
+type Address = {
+ postCode: string;
+ description: string;
+ address: string;
+};
+
+type User = {
+ id: number;
+ name: string;
+ address: Address;
+};
+
+type FlattenedUser = FlattenType;
+type FlattenedUserKeys = keyof FlattenType;
+
+export type FlattenTypeKeys = keyof {
+ [Key in keyof Source as Key extends string
+ ? Source[Key] extends object
+ ? ` + "`${Key}.${keyof FlattenType & string}`" + `
+ : Key
+ : never]-?: Target;
+};
+
+type FlattenedUserKeys2 = FlattenTypeKeys;`
+
+ fs := vfstest.FromMap(map[string]string{
+ "/test.ts": content,
+ "/tsconfig.json": `{
+ "compilerOptions": {
+ "strict": true,
+ "noEmit": true
+ },
+ "files": ["test.ts"]
+ }`,
+ }, false)
+ fs = bundled.WrapFS(fs)
+
+ cd := "/"
+ host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil)
+
+ parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, host, nil)
+ assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")
+
+ p := compiler.NewProgram(compiler.ProgramOptions{
+ Config: parsed,
+ Host: host,
+ })
+ p.BindSourceFiles()
+ c, done := p.GetTypeChecker(t.Context())
+ defer done()
+
+ // The test passes if we can get a type checker without crashing
+ assert.Assert(t, c != nil)
+}
+
func BenchmarkNewChecker(b *testing.B) {
repo.SkipIfNoTypeScriptSubmodule(b)
fs := osvfs.FS()