From b519199ac6b759904b74e000c8188455d1a29e8e Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 12 Sep 2026 18:47:45 +0530 Subject: [PATCH 1/3] fix reverse mapped type inference when all properties are context-senstive --- tsc/internal/checker/inference.go | 11 +- ...reverseMappedPartiallyInferableTypes.types | 4 +- ...eWithAllContextSensitiveProperties.symbols | 110 ++++++++++++++++++ ...ypeWithAllContextSensitiveProperties.types | 91 +++++++++++++++ ...edTypeWithAllContextSensitiveProperties.ts | 39 +++++++ 5 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types create mode 100644 tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index 4bb9d2923d9ab..7a516465666c9 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -1053,14 +1053,11 @@ func (c *Checker) createReverseMappedType(source *Type, target *Type, constraint return reversed } -// We consider a type to be partially inferable if it isn't marked non-inferable or if it is -// an object literal type with at least one property of an inferable type. For example, an object -// literal { a: 123, b: x => true } is marked non-inferable because it contains a context sensitive -// arrow function, but is considered partially inferable because property 'a' has an inferable type. +// We consider a type to be partially inferable if it isn't marked non-inferable or if it is a +// non-empty object literal. Even when every property is context sensitive, reverse mapped types can +// still infer the object's keys and defer inference from its property values. func (c *Checker) isPartiallyInferableType(t *Type) bool { - return t.objectFlags&ObjectFlagsNonInferrableType == 0 || isObjectLiteralType(t) && core.Some(c.getPropertiesOfType(t), func(prop *ast.Symbol) bool { - return c.isPartiallyInferableType(c.getTypeOfSymbol(prop)) - }) || isTupleType(t) && core.Some(c.getElementTypes(t), c.isPartiallyInferableType) + return t.objectFlags&ObjectFlagsNonInferrableType == 0 || isObjectLiteralType(t) && len(c.getPropertiesOfType(t)) != 0 || isTupleType(t) && core.Some(c.getElementTypes(t), c.isPartiallyInferableType) } func (c *Checker) inferReverseMappedType(source *Type, target *Type, constraint *Type) *Type { diff --git a/tsc/testdata/baselines/reference/compiler/reverseMappedPartiallyInferableTypes.types b/tsc/testdata/baselines/reference/compiler/reverseMappedPartiallyInferableTypes.types index c25702df0bbf5..4f80a4747c819 100644 --- a/tsc/testdata/baselines/reference/compiler/reverseMappedPartiallyInferableTypes.types +++ b/tsc/testdata/baselines/reference/compiler/reverseMappedPartiallyInferableTypes.types @@ -208,8 +208,8 @@ const obj2 = id({ // No properties have inferable types const obj3 = id({ ->obj3 : Mapped ->id({ foo: { contains(k) { return k.length > 0; } }}) : Mapped +>obj3 : Mapped<{ foo: unknown; }> +>id({ foo: { contains(k) { return k.length > 0; } }}) : Mapped<{ foo: unknown; }> >id : (arg: Mapped) => Mapped >{ foo: { contains(k) { return k.length > 0; } }} : { foo: { contains(k: unknown): boolean; }; } diff --git a/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols new file mode 100644 index 0000000000000..3dd50eca81ce4 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols @@ -0,0 +1,110 @@ +//// [tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts] //// + +=== reverseMappedTypeWithAllContextSensitiveProperties.ts === +createMachine({ +>createMachine : Symbol(createMachine, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 13, 13)) + + initial: "a", +>initial : Symbol(initial, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 0, 15)) + + context: { foo: 1 }, +>context : Symbol(context, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 1, 15)) +>foo : Symbol(foo, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 2, 12)) + + states: { +>states : Symbol(states, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 2, 22)) + + a: { +>a : Symbol(a, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 3, 11)) + + entry: (context, state) => { +>entry : Symbol(entry, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 4, 8)) +>context : Symbol(context, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 5, 14)) +>state : Symbol(state, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 5, 22)) + + context.foo.toFixed(); +>context.foo.toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) +>context.foo : Symbol(foo, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 2, 12)) +>context : Symbol(context, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 5, 14)) +>foo : Symbol(foo, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 2, 12)) +>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) + + const currentState: "a" = state; +>currentState : Symbol(currentState, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 7, 13)) +>state : Symbol(state, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 5, 22)) + } + } + } +}) + +declare const createMachine: +>createMachine : Symbol(createMachine, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 13, 13)) + + D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) + + initial: keyof D["states"], +>initial : Symbol(initial, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 14)) +>D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) + + context: object, +>context : Symbol(context, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 15, 31)) + + states: { +>states : Symbol(states, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 16, 20)) + + [S in keyof D["states"]]: { +>S : Symbol(S, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 18, 7)) +>D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) + + entry?: (context: D["context"], state: S) => void +>entry : Symbol(entry, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 18, 33)) +>context : Symbol(context, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 19, 17)) +>D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) +>state : Symbol(state, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 19, 39)) +>S : Symbol(S, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 18, 7)) + } + } + }> + (definition: IdentityObject) => +>definition : Symbol(definition, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 23, 5)) +>IdentityObject : Symbol(IdentityObject, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 32, 11)) +>D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) + + D +>D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) + +type Identity = +>Identity : Symbol(Identity, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 24, 7)) +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 26, 14)) + + T extends any +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 26, 14)) + + ? ( T extends (...a: never) => unknown ? T : +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 26, 14)) +>a : Symbol(a, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 28, 19)) +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 26, 14)) + + T extends object ? IdentityObject : +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 26, 14)) +>IdentityObject : Symbol(IdentityObject, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 32, 11)) +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 26, 14)) + + T +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 26, 14)) + + ) + : never + +type IdentityObject = +>IdentityObject : Symbol(IdentityObject, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 32, 11)) +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 34, 20)) + + { [K in keyof T]: Identity } +>K : Symbol(K, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 35, 5)) +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 34, 20)) +>Identity : Symbol(Identity, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 24, 7)) +>T : Symbol(T, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 34, 20)) +>K : Symbol(K, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 35, 5)) + diff --git a/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types new file mode 100644 index 0000000000000..ecbb058250fc4 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types @@ -0,0 +1,91 @@ +//// [tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts] //// + +=== reverseMappedTypeWithAllContextSensitiveProperties.ts === +createMachine({ +>createMachine({ initial: "a", context: { foo: 1 }, states: { a: { entry: (context, state) => { context.foo.toFixed(); const currentState: "a" = state; } } }}) : { initial: "a"; context: object; states: { a: { entry?: ((context: { foo: number; }, state: "a") => void) | undefined; }; }; } +>createMachine : void; }; }; }>(definition: IdentityObject) => D +>{ initial: "a", context: { foo: 1 }, states: { a: { entry: (context, state) => { context.foo.toFixed(); const currentState: "a" = state; } } }} : { initial: "a"; context: { foo: number; }; states: { a: { entry: (context: { foo: number; }, state: "a") => void; }; }; } + + initial: "a", +>initial : "a" +>"a" : "a" + + context: { foo: 1 }, +>context : { foo: number; } +>{ foo: 1 } : { foo: number; } +>foo : number +>1 : 1 + + states: { +>states : { a: { entry: (context: { foo: number; }, state: "a") => void; }; } +>{ a: { entry: (context, state) => { context.foo.toFixed(); const currentState: "a" = state; } } } : { a: { entry: (context: { foo: number; }, state: "a") => void; }; } + + a: { +>a : { entry: (context: { foo: number; }, state: "a") => void; } +>{ entry: (context, state) => { context.foo.toFixed(); const currentState: "a" = state; } } : { entry: (context: { foo: number; }, state: "a") => void; } + + entry: (context, state) => { +>entry : (context: { foo: number; }, state: "a") => void +>(context, state) => { context.foo.toFixed(); const currentState: "a" = state; } : (context: { foo: number; }, state: "a") => void +>context : { foo: number; } +>state : "a" + + context.foo.toFixed(); +>context.foo.toFixed() : string +>context.foo.toFixed : (fractionDigits?: number) => string +>context.foo : number +>context : { foo: number; } +>foo : number +>toFixed : (fractionDigits?: number) => string + + const currentState: "a" = state; +>currentState : "a" +>state : "a" + } + } + } +}) + +declare const createMachine: +>createMachine : void; }; }; }>(definition: IdentityObject) => D + + initial : keyof D["states"] + + context: object, +>context : object + + states: { +>states : { [S in keyof D["states"]]: { entry?: (context: D["context"], state: S) => void; }; } + + [S in keyof D["states"]]: { + entry?: (context: D["context"], state: S) => void +>entry : ((context: D["context"], state: S) => void) | undefined +>context : D["context"] +>state : S + } + } + }> + (definition: IdentityObject) => +>definition : IdentityObject + + D + +type Identity = +>Identity : Identity + + T extends any + ? ( T extends (...a: never) => unknown ? T : +>a : never + + T extends object ? IdentityObject : + T + ) + : never + +type IdentityObject = +>IdentityObject : IdentityObject + + { [K in keyof T]: Identity } + diff --git a/tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts b/tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts new file mode 100644 index 0000000000000..9123117e958da --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts @@ -0,0 +1,39 @@ +// @strict: true +// @noEmit: true + +createMachine({ + initial: "a", + context: { foo: 1 }, + states: { + a: { + entry: (context, state) => { + context.foo.toFixed(); + const currentState: "a" = state; + } + } + } +}) + +declare const createMachine: + void + } + } + }> + (definition: IdentityObject) => + D + +type Identity = + T extends any + ? ( T extends (...a: never) => unknown ? T : + T extends object ? IdentityObject : + T + ) + : never + +type IdentityObject = + { [K in keyof T]: Identity } From 08eb3708955ad911c6ec527a77d2ac9fd604f044 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 12 Sep 2026 19:02:12 +0530 Subject: [PATCH 2/3] make sure the context is infered in the return type too --- ...MappedTypeWithAllContextSensitiveProperties.symbols | 6 ++++-- ...seMappedTypeWithAllContextSensitiveProperties.types | 10 +++++----- ...verseMappedTypeWithAllContextSensitiveProperties.ts | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols index 3dd50eca81ce4..4f35da315ed43 100644 --- a/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols +++ b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.symbols @@ -47,11 +47,13 @@ declare const createMachine: >initial : Symbol(initial, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 14)) >D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) - context: object, + context: D["context"] extends object ? D["context"] : object, >context : Symbol(context, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 15, 31)) +>D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) +>D : Symbol(D, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 14, 3)) states: { ->states : Symbol(states, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 16, 20)) +>states : Symbol(states, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 16, 65)) [S in keyof D["states"]]: { >S : Symbol(S, Decl(reverseMappedTypeWithAllContextSensitiveProperties.ts, 18, 7)) diff --git a/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types index ecbb058250fc4..b37dfb6640a45 100644 --- a/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types +++ b/tsc/testdata/baselines/reference/compiler/reverseMappedTypeWithAllContextSensitiveProperties.types @@ -2,8 +2,8 @@ === reverseMappedTypeWithAllContextSensitiveProperties.ts === createMachine({ ->createMachine({ initial: "a", context: { foo: 1 }, states: { a: { entry: (context, state) => { context.foo.toFixed(); const currentState: "a" = state; } } }}) : { initial: "a"; context: object; states: { a: { entry?: ((context: { foo: number; }, state: "a") => void) | undefined; }; }; } ->createMachine : void; }; }; }>(definition: IdentityObject) => D +>createMachine({ initial: "a", context: { foo: 1 }, states: { a: { entry: (context, state) => { context.foo.toFixed(); const currentState: "a" = state; } } }}) : { initial: "a"; context: { foo: number; }; states: { a: { entry?: ((context: { foo: number; }, state: "a") => void) | undefined; }; }; } +>createMachine : void; }; }; }>(definition: IdentityObject) => D >{ initial: "a", context: { foo: 1 }, states: { a: { entry: (context, state) => { context.foo.toFixed(); const currentState: "a" = state; } } }} : { initial: "a"; context: { foo: number; }; states: { a: { entry: (context: { foo: number; }, state: "a") => void; }; }; } initial: "a", @@ -47,14 +47,14 @@ createMachine({ }) declare const createMachine: ->createMachine : void; }; }; }>(definition: IdentityObject) => D +>createMachine : void; }; }; }>(definition: IdentityObject) => D initial : keyof D["states"] - context: object, ->context : object + context: D["context"] extends object ? D["context"] : object, +>context : D["context"] extends object ? D["context"] : object states: { >states : { [S in keyof D["states"]]: { entry?: (context: D["context"], state: S) => void; }; } diff --git a/tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts b/tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts index 9123117e958da..64b80015c493d 100644 --- a/tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts +++ b/tsc/testdata/tests/cases/compiler/reverseMappedTypeWithAllContextSensitiveProperties.ts @@ -17,7 +17,7 @@ createMachine({ declare const createMachine: void From 3b7443e10fc56dd386ab14d9379622c16bd4105b Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 12 Sep 2026 22:46:02 +0530 Subject: [PATCH 3/3] go all in --- tsc/internal/checker/inference.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tsc/internal/checker/inference.go b/tsc/internal/checker/inference.go index 7a516465666c9..ab30343d2f734 100644 --- a/tsc/internal/checker/inference.go +++ b/tsc/internal/checker/inference.go @@ -1053,11 +1053,10 @@ func (c *Checker) createReverseMappedType(source *Type, target *Type, constraint return reversed } -// We consider a type to be partially inferable if it isn't marked non-inferable or if it is a -// non-empty object literal. Even when every property is context sensitive, reverse mapped types can -// still infer the object's keys and defer inference from its property values. +// We consider a type to be partially inferable if it isn't marked non-inferable. Even when every property is context sensitive, +// reverse mapped types can still infer the object's keys and defer inference from its property values. func (c *Checker) isPartiallyInferableType(t *Type) bool { - return t.objectFlags&ObjectFlagsNonInferrableType == 0 || isObjectLiteralType(t) && len(c.getPropertiesOfType(t)) != 0 || isTupleType(t) && core.Some(c.getElementTypes(t), c.isPartiallyInferableType) + return t.objectFlags&ObjectFlagsNonInferrableType == 0 || isObjectLiteralType(t) || isTupleType(t) } func (c *Checker) inferReverseMappedType(source *Type, target *Type, constraint *Type) *Type {