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()