From 8485fc06f9ca7e8f402cbe8bacbc0209c7b534cb Mon Sep 17 00:00:00 2001 From: "arash.mousavi" Date: Sat, 20 Sep 2025 16:33:49 +0330 Subject: [PATCH 01/11] fix(checker): contextual typing by discriminant in object literals (fixes #1727) --- internal/checker/inference.go | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index d395f03a98..bee50a6732 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -239,6 +239,10 @@ func (c *Checker) inferFromTypes(n *InferenceState, source *Type, target *Type) case source.flags&TypeFlagsIndexedAccess != 0 && target.flags&TypeFlagsIndexedAccess != 0: c.inferFromTypes(n, source.AsIndexedAccessType().objectType, target.AsIndexedAccessType().objectType) c.inferFromTypes(n, source.AsIndexedAccessType().indexType, target.AsIndexedAccessType().indexType) + case isLiteralType(source) && target.flags&TypeFlagsIndexedAccess != 0: + // Handle reverse inference: when source is a literal type and target is T['property'], + // try to infer T based on the constraint that T['property'] = source + c.inferFromLiteralToIndexedAccess(n, source, target.AsIndexedAccessType()) case source.flags&TypeFlagsStringMapping != 0 && target.flags&TypeFlagsStringMapping != 0: if source.symbol == target.symbol { c.inferFromTypes(n, source.AsStringMappingType().target, target.AsStringMappingType().target) @@ -1605,3 +1609,69 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn } } } + +// inferFromLiteralToIndexedAccess performs reverse inference from a literal type to an indexed access type. +// When we have a literal value being assigned to T['property'], we can infer that T must be a type where +// T['property'] equals the literal value. This is used for discriminated union type inference. +func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Type, target *IndexedAccessType) { + // Only proceed if the object type is a type parameter that we're inferring + objectType := target.objectType + if objectType.flags&TypeFlagsTypeParameter == 0 { + return + } + + // Get the inference info for the type parameter + inference := getInferenceInfoForType(n, objectType) + if inference == nil || inference.isFixed { + return + } + + // Get the constraint of the type parameter (e.g., ASTNode) + constraint := c.getBaseConstraintOfType(inference.typeParameter) + if constraint == nil { + return + } + + // Only handle union constraints (discriminated unions) + if constraint.flags&TypeFlagsUnion == 0 { + return + } + + // Look for a union member where the indexed access type matches the source literal + indexType := target.indexType + for _, unionMember := range constraint.Types() { + // Try to get the type of the indexed property from this union member + memberIndexedType := c.getIndexedAccessType(unionMember, indexType) + + // Skip if we can't resolve the indexed access + if memberIndexedType == nil || c.isErrorType(memberIndexedType) { + continue + } + + // Check if this member's indexed property type matches our literal source + if c.isTypeIdenticalTo(source, memberIndexedType) { + // Found a match! Infer this union member as a candidate for the type parameter + candidate := core.OrElse(n.propagationType, unionMember) + if candidate == c.blockedStringType { + return + } + + if n.priority < inference.priority { + inference.candidates = nil + inference.contraCandidates = nil + inference.topLevel = true + inference.priority = n.priority + } + + if n.priority == inference.priority { + if !slices.Contains(inference.candidates, candidate) { + inference.candidates = append(inference.candidates, candidate) + clearCachedInferences(n.inferences) + } + } + + n.inferencePriority = min(n.inferencePriority, n.priority) + return + } + } +} From 86c883065ace9db3999f3d3321f16819da8e0a5c Mon Sep 17 00:00:00 2001 From: Arash <75931882+arash-mosavi@users.noreply.github.com> Date: Sat, 20 Sep 2025 19:12:28 +0330 Subject: [PATCH 02/11] Update internal/checker/inference.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/checker/inference.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index bee50a6732..2cbdb52032 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1651,7 +1651,7 @@ func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Typ // Check if this member's indexed property type matches our literal source if c.isTypeIdenticalTo(source, memberIndexedType) { // Found a match! Infer this union member as a candidate for the type parameter - candidate := core.OrElse(n.propagationType, unionMember) + candidate := unionMember if candidate == c.blockedStringType { return } From f3c8dca33a9e58062d1706e02476ba5fb786d829 Mon Sep 17 00:00:00 2001 From: "arash.mousavi" Date: Sat, 20 Sep 2025 19:26:00 +0330 Subject: [PATCH 03/11] fix(checker): improve inference handling for indexed access types --- internal/checker/inference.go | 92 +++++++++++++++++------------------ 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index 2cbdb52032..a08646f84e 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1616,62 +1616,60 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Type, target *IndexedAccessType) { // Only proceed if the object type is a type parameter that we're inferring objectType := target.objectType - if objectType.flags&TypeFlagsTypeParameter == 0 { - return - } - - // Get the inference info for the type parameter - inference := getInferenceInfoForType(n, objectType) - if inference == nil || inference.isFixed { - return - } - - // Get the constraint of the type parameter (e.g., ASTNode) - constraint := c.getBaseConstraintOfType(inference.typeParameter) - if constraint == nil { - return - } - - // Only handle union constraints (discriminated unions) - if constraint.flags&TypeFlagsUnion == 0 { - return - } + if objectType.flags&TypeFlagsTypeParameter != 0 { + // Get the inference info for the type parameter + inference := getInferenceInfoForType(n, objectType) + if inference == nil || inference.isFixed { + return + } - // Look for a union member where the indexed access type matches the source literal - indexType := target.indexType - for _, unionMember := range constraint.Types() { - // Try to get the type of the indexed property from this union member - memberIndexedType := c.getIndexedAccessType(unionMember, indexType) + // Get the constraint of the type parameter (e.g., ASTNode) + constraint := c.getBaseConstraintOfType(inference.typeParameter) + if constraint == nil { + return + } - // Skip if we can't resolve the indexed access - if memberIndexedType == nil || c.isErrorType(memberIndexedType) { - continue + // Only handle union constraints (discriminated unions) + if constraint.flags&TypeFlagsUnion == 0 { + return } - // Check if this member's indexed property type matches our literal source - if c.isTypeIdenticalTo(source, memberIndexedType) { - // Found a match! Infer this union member as a candidate for the type parameter - candidate := unionMember - if candidate == c.blockedStringType { - return - } + // Look for a union member where the indexed access type matches the source literal + indexType := target.indexType + for _, unionMember := range constraint.Types() { + // Try to get the type of the indexed property from this union member + memberIndexedType := c.getIndexedAccessType(unionMember, indexType) - if n.priority < inference.priority { - inference.candidates = nil - inference.contraCandidates = nil - inference.topLevel = true - inference.priority = n.priority + // Skip if we can't resolve the indexed access + if memberIndexedType == nil || c.isErrorType(memberIndexedType) { + continue } - if n.priority == inference.priority { - if !slices.Contains(inference.candidates, candidate) { - inference.candidates = append(inference.candidates, candidate) - clearCachedInferences(n.inferences) + // Check if this member's indexed property type matches our literal source + if c.isTypeIdenticalTo(source, memberIndexedType) { + // Found a match! Infer this union member as a candidate for the type parameter + candidate := unionMember + if candidate == c.blockedStringType { + return } - } - n.inferencePriority = min(n.inferencePriority, n.priority) - return + if n.priority < inference.priority { + inference.candidates = nil + inference.contraCandidates = nil + inference.topLevel = true + inference.priority = n.priority + } + + if n.priority == inference.priority { + if !slices.Contains(inference.candidates, candidate) { + inference.candidates = append(inference.candidates, candidate) + clearCachedInferences(n.inferences) + } + } + + n.inferencePriority = min(n.inferencePriority, n.priority) + return + } } } } From 94f61a6936c58008cc77f7bafad8aac8fac8af0c Mon Sep 17 00:00:00 2001 From: "arash.mousavi" Date: Sat, 20 Sep 2025 19:32:25 +0330 Subject: [PATCH 04/11] Add test case and baselines for cssTreeTypeInference - Add cssTreeTypeInference.ts test case to verify discriminated union type inference - Include baseline files showing expected type inference behavior - Ensures node parameter is correctly inferred as Declaration type in contextual typing scenarios --- .../compiler/cssTreeTypeInference.js | 72 ++++++++++ .../compiler/cssTreeTypeInference.symbols | 128 ++++++++++++++++++ .../compiler/cssTreeTypeInference.types | 120 ++++++++++++++++ .../cases/compiler/cssTreeTypeInference.ts | 52 +++++++ 4 files changed, 372 insertions(+) create mode 100644 testdata/baselines/reference/compiler/cssTreeTypeInference.js create mode 100644 testdata/baselines/reference/compiler/cssTreeTypeInference.symbols create mode 100644 testdata/baselines/reference/compiler/cssTreeTypeInference.types create mode 100644 testdata/tests/cases/compiler/cssTreeTypeInference.ts diff --git a/testdata/baselines/reference/compiler/cssTreeTypeInference.js b/testdata/baselines/reference/compiler/cssTreeTypeInference.js new file mode 100644 index 0000000000..a25455200e --- /dev/null +++ b/testdata/baselines/reference/compiler/cssTreeTypeInference.js @@ -0,0 +1,72 @@ +//// [tests/cases/compiler/cssTreeTypeInference.ts] //// + +//// [cssTreeTypeInference.ts] +// Simplified reproduction of css-tree type inference issue +// https://github.com/microsoft/typescript-go/issues/1727 + +interface Declaration { + type: 'Declaration'; + property: string; + value: string; +} + +interface Rule { + type: 'Rule'; + selector: string; + children: Declaration[]; +} + +type ASTNode = Declaration | Rule; + +interface WalkOptions { + visit: T['type']; + enter(node: T): void; +} + +declare function walk(ast: ASTNode, options: WalkOptions): void; + +// Test case 1: Simple type inference +const ast: ASTNode = { + type: 'Declaration', + property: 'color', + value: 'red' +}; + +// This should infer node as Declaration type +walk(ast, { + visit: 'Declaration', + enter(node) { + console.log(node.property); // Should not error - node should be inferred as Declaration + }, +}); + +// Test case 2: More complex scenario +declare const complexAst: Rule; + +walk(complexAst, { + visit: 'Declaration', + enter(node) { + console.log(node.value); // Should infer node as Declaration + }, +}); + +//// [cssTreeTypeInference.js] +// Test case 1: Simple type inference +const ast = { + type: 'Declaration', + property: 'color', + value: 'red' +}; +// This should infer node as Declaration type +walk(ast, { + visit: 'Declaration', + enter(node) { + console.log(node.property); // Should not error - node should be inferred as Declaration + }, +}); +walk(complexAst, { + visit: 'Declaration', + enter(node) { + console.log(node.value); // Should infer node as Declaration + }, +}); diff --git a/testdata/baselines/reference/compiler/cssTreeTypeInference.symbols b/testdata/baselines/reference/compiler/cssTreeTypeInference.symbols new file mode 100644 index 0000000000..fee5c92956 --- /dev/null +++ b/testdata/baselines/reference/compiler/cssTreeTypeInference.symbols @@ -0,0 +1,128 @@ +//// [tests/cases/compiler/cssTreeTypeInference.ts] //// + +=== cssTreeTypeInference.ts === +// Simplified reproduction of css-tree type inference issue +// https://github.com/microsoft/typescript-go/issues/1727 + +interface Declaration { +>Declaration : Symbol(Declaration, Decl(cssTreeTypeInference.ts, 0, 0)) + + type: 'Declaration'; +>type : Symbol(Declaration.type, Decl(cssTreeTypeInference.ts, 3, 23)) + + property: string; +>property : Symbol(Declaration.property, Decl(cssTreeTypeInference.ts, 4, 24)) + + value: string; +>value : Symbol(Declaration.value, Decl(cssTreeTypeInference.ts, 5, 21)) +} + +interface Rule { +>Rule : Symbol(Rule, Decl(cssTreeTypeInference.ts, 7, 1)) + + type: 'Rule'; +>type : Symbol(Rule.type, Decl(cssTreeTypeInference.ts, 9, 16)) + + selector: string; +>selector : Symbol(Rule.selector, Decl(cssTreeTypeInference.ts, 10, 17)) + + children: Declaration[]; +>children : Symbol(Rule.children, Decl(cssTreeTypeInference.ts, 11, 21)) +>Declaration : Symbol(Declaration, Decl(cssTreeTypeInference.ts, 0, 0)) +} + +type ASTNode = Declaration | Rule; +>ASTNode : Symbol(ASTNode, Decl(cssTreeTypeInference.ts, 13, 1)) +>Declaration : Symbol(Declaration, Decl(cssTreeTypeInference.ts, 0, 0)) +>Rule : Symbol(Rule, Decl(cssTreeTypeInference.ts, 7, 1)) + +interface WalkOptions { +>WalkOptions : Symbol(WalkOptions, Decl(cssTreeTypeInference.ts, 15, 34)) +>T : Symbol(T, Decl(cssTreeTypeInference.ts, 17, 22)) +>ASTNode : Symbol(ASTNode, Decl(cssTreeTypeInference.ts, 13, 1)) + + visit: T['type']; +>visit : Symbol(WalkOptions.visit, Decl(cssTreeTypeInference.ts, 17, 42)) +>T : Symbol(T, Decl(cssTreeTypeInference.ts, 17, 22)) + + enter(node: T): void; +>enter : Symbol(WalkOptions.enter, Decl(cssTreeTypeInference.ts, 18, 21)) +>node : Symbol(node, Decl(cssTreeTypeInference.ts, 19, 10)) +>T : Symbol(T, Decl(cssTreeTypeInference.ts, 17, 22)) +} + +declare function walk(ast: ASTNode, options: WalkOptions): void; +>walk : Symbol(walk, Decl(cssTreeTypeInference.ts, 20, 1)) +>T : Symbol(T, Decl(cssTreeTypeInference.ts, 22, 22)) +>ASTNode : Symbol(ASTNode, Decl(cssTreeTypeInference.ts, 13, 1)) +>ast : Symbol(ast, Decl(cssTreeTypeInference.ts, 22, 41)) +>ASTNode : Symbol(ASTNode, Decl(cssTreeTypeInference.ts, 13, 1)) +>options : Symbol(options, Decl(cssTreeTypeInference.ts, 22, 54)) +>WalkOptions : Symbol(WalkOptions, Decl(cssTreeTypeInference.ts, 15, 34)) +>T : Symbol(T, Decl(cssTreeTypeInference.ts, 22, 22)) + +// Test case 1: Simple type inference +const ast: ASTNode = { +>ast : Symbol(ast, Decl(cssTreeTypeInference.ts, 25, 5)) +>ASTNode : Symbol(ASTNode, Decl(cssTreeTypeInference.ts, 13, 1)) + + type: 'Declaration', +>type : Symbol(type, Decl(cssTreeTypeInference.ts, 25, 22)) + + property: 'color', +>property : Symbol(property, Decl(cssTreeTypeInference.ts, 26, 24)) + + value: 'red' +>value : Symbol(value, Decl(cssTreeTypeInference.ts, 27, 22)) + +}; + +// This should infer node as Declaration type +walk(ast, { +>walk : Symbol(walk, Decl(cssTreeTypeInference.ts, 20, 1)) +>ast : Symbol(ast, Decl(cssTreeTypeInference.ts, 25, 5)) + + visit: 'Declaration', +>visit : Symbol(visit, Decl(cssTreeTypeInference.ts, 32, 11)) + + enter(node) { +>enter : Symbol(enter, Decl(cssTreeTypeInference.ts, 33, 25)) +>node : Symbol(node, Decl(cssTreeTypeInference.ts, 34, 10)) + + console.log(node.property); // Should not error - node should be inferred as Declaration +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>node.property : Symbol(Declaration.property, Decl(cssTreeTypeInference.ts, 4, 24)) +>node : Symbol(node, Decl(cssTreeTypeInference.ts, 34, 10)) +>property : Symbol(Declaration.property, Decl(cssTreeTypeInference.ts, 4, 24)) + + }, +}); + +// Test case 2: More complex scenario +declare const complexAst: Rule; +>complexAst : Symbol(complexAst, Decl(cssTreeTypeInference.ts, 40, 13)) +>Rule : Symbol(Rule, Decl(cssTreeTypeInference.ts, 7, 1)) + +walk(complexAst, { +>walk : Symbol(walk, Decl(cssTreeTypeInference.ts, 20, 1)) +>complexAst : Symbol(complexAst, Decl(cssTreeTypeInference.ts, 40, 13)) + + visit: 'Declaration', +>visit : Symbol(visit, Decl(cssTreeTypeInference.ts, 42, 18)) + + enter(node) { +>enter : Symbol(enter, Decl(cssTreeTypeInference.ts, 43, 25)) +>node : Symbol(node, Decl(cssTreeTypeInference.ts, 44, 10)) + + console.log(node.value); // Should infer node as Declaration +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>node.value : Symbol(Declaration.value, Decl(cssTreeTypeInference.ts, 5, 21)) +>node : Symbol(node, Decl(cssTreeTypeInference.ts, 44, 10)) +>value : Symbol(Declaration.value, Decl(cssTreeTypeInference.ts, 5, 21)) + + }, +}); diff --git a/testdata/baselines/reference/compiler/cssTreeTypeInference.types b/testdata/baselines/reference/compiler/cssTreeTypeInference.types new file mode 100644 index 0000000000..096a9bf57c --- /dev/null +++ b/testdata/baselines/reference/compiler/cssTreeTypeInference.types @@ -0,0 +1,120 @@ +//// [tests/cases/compiler/cssTreeTypeInference.ts] //// + +=== cssTreeTypeInference.ts === +// Simplified reproduction of css-tree type inference issue +// https://github.com/microsoft/typescript-go/issues/1727 + +interface Declaration { + type: 'Declaration'; +>type : "Declaration" + + property: string; +>property : string + + value: string; +>value : string +} + +interface Rule { + type: 'Rule'; +>type : "Rule" + + selector: string; +>selector : string + + children: Declaration[]; +>children : Declaration[] +} + +type ASTNode = Declaration | Rule; +>ASTNode : ASTNode + +interface WalkOptions { + visit: T['type']; +>visit : T["type"] + + enter(node: T): void; +>enter : (node: T) => void +>node : T +} + +declare function walk(ast: ASTNode, options: WalkOptions): void; +>walk : (ast: ASTNode, options: WalkOptions) => void +>ast : ASTNode +>options : WalkOptions + +// Test case 1: Simple type inference +const ast: ASTNode = { +>ast : ASTNode +>{ type: 'Declaration', property: 'color', value: 'red'} : { type: "Declaration"; property: string; value: string; } + + type: 'Declaration', +>type : "Declaration" +>'Declaration' : "Declaration" + + property: 'color', +>property : string +>'color' : "color" + + value: 'red' +>value : string +>'red' : "red" + +}; + +// This should infer node as Declaration type +walk(ast, { +>walk(ast, { visit: 'Declaration', enter(node) { console.log(node.property); // Should not error - node should be inferred as Declaration },}) : void +>walk : (ast: ASTNode, options: WalkOptions) => void +>ast : Declaration +>{ visit: 'Declaration', enter(node) { console.log(node.property); // Should not error - node should be inferred as Declaration },} : { visit: "Declaration"; enter(node: Declaration): void; } + + visit: 'Declaration', +>visit : "Declaration" +>'Declaration' : "Declaration" + + enter(node) { +>enter : (node: Declaration) => void +>node : Declaration + + console.log(node.property); // Should not error - node should be inferred as Declaration +>console.log(node.property) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>node.property : string +>node : Declaration +>property : string + + }, +}); + +// Test case 2: More complex scenario +declare const complexAst: Rule; +>complexAst : Rule + +walk(complexAst, { +>walk(complexAst, { visit: 'Declaration', enter(node) { console.log(node.value); // Should infer node as Declaration },}) : void +>walk : (ast: ASTNode, options: WalkOptions) => void +>complexAst : Rule +>{ visit: 'Declaration', enter(node) { console.log(node.value); // Should infer node as Declaration },} : { visit: "Declaration"; enter(node: Declaration): void; } + + visit: 'Declaration', +>visit : "Declaration" +>'Declaration' : "Declaration" + + enter(node) { +>enter : (node: Declaration) => void +>node : Declaration + + console.log(node.value); // Should infer node as Declaration +>console.log(node.value) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>node.value : string +>node : Declaration +>value : string + + }, +}); diff --git a/testdata/tests/cases/compiler/cssTreeTypeInference.ts b/testdata/tests/cases/compiler/cssTreeTypeInference.ts new file mode 100644 index 0000000000..d27ba9030b --- /dev/null +++ b/testdata/tests/cases/compiler/cssTreeTypeInference.ts @@ -0,0 +1,52 @@ +// @strict: true +// @module: commonjs +// @target: es2015 + +// Simplified reproduction of css-tree type inference issue +// https://github.com/microsoft/typescript-go/issues/1727 + +interface Declaration { + type: 'Declaration'; + property: string; + value: string; +} + +interface Rule { + type: 'Rule'; + selector: string; + children: Declaration[]; +} + +type ASTNode = Declaration | Rule; + +interface WalkOptions { + visit: T['type']; + enter(node: T): void; +} + +declare function walk(ast: ASTNode, options: WalkOptions): void; + +// Test case 1: Simple type inference +const ast: ASTNode = { + type: 'Declaration', + property: 'color', + value: 'red' +}; + +// This should infer node as Declaration type +walk(ast, { + visit: 'Declaration', + enter(node) { + console.log(node.property); // Should not error - node should be inferred as Declaration + }, +}); + +// Test case 2: More complex scenario +declare const complexAst: Rule; + +walk(complexAst, { + visit: 'Declaration', + enter(node) { + console.log(node.value); // Should infer node as Declaration + }, +}); \ No newline at end of file From 71a13799c14faea3be7c773091247c41014c8b43 Mon Sep 17 00:00:00 2001 From: Arash <75931882+arash-mosavi@users.noreply.github.com> Date: Sat, 20 Sep 2025 20:29:31 +0330 Subject: [PATCH 05/11] Update internal/checker/inference.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/checker/inference.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index a08646f84e..f4447c1cc5 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1610,9 +1610,26 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn } } -// inferFromLiteralToIndexedAccess performs reverse inference from a literal type to an indexed access type. -// When we have a literal value being assigned to T['property'], we can infer that T must be a type where -// T['property'] equals the literal value. This is used for discriminated union type inference. +// inferFromLiteralToIndexedAccess implements a reverse inference algorithm for indexed access types. +// +// This function is used during type inference when a literal value is assigned to an indexed access type, +// such as in the pattern `T['type'] = 'Declaration'`. In this scenario, we can infer that the type parameter +// `T` must be a type where the property `'type'` has the value `'Declaration'`. This is particularly useful +// for discriminated union type inference, where the discriminant property (e.g., 'type') determines the +// specific union member. +// +// Example: +// Given a union type: +// type Node = { type: 'Declaration', ... } | { type: 'Expression', ... } +// and an assignment: +// T['type'] = 'Declaration' +// this function infers that T = { type: 'Declaration', ... }. +// +// The algorithm works by: +// 1. Checking if the object type of the indexed access is a type parameter being inferred. +// 2. Looking at the constraint of the type parameter (typically a union). +// 3. For each union member, checking if the indexed property type matches the source literal. +// 4. If a match is found, inferring that union member as a candidate for the type parameter. func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Type, target *IndexedAccessType) { // Only proceed if the object type is a type parameter that we're inferring objectType := target.objectType From ac20851b93e2615b8a5d3f9e533a8f44c26921f4 Mon Sep 17 00:00:00 2001 From: Arash <75931882+arash-mosavi@users.noreply.github.com> Date: Sat, 20 Sep 2025 20:29:43 +0330 Subject: [PATCH 06/11] Update internal/checker/inference.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/checker/inference.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index f4447c1cc5..fecdaa792e 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1666,6 +1666,10 @@ func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Typ if c.isTypeIdenticalTo(source, memberIndexedType) { // Found a match! Infer this union member as a candidate for the type parameter candidate := unionMember + // Prevent inferring the blocked string type as a candidate. + // This type is used as a sentinel to represent cases where string inference should not occur, + // such as when a string index signature would lead to overly broad or incorrect inference. + // Blocking it here avoids unsound or unintended type inference results. if candidate == c.blockedStringType { return } From 5a88d66c6fb1cef7ebb1c34d025e27fad5d55d29 Mon Sep 17 00:00:00 2001 From: "arash.mousavi" Date: Sat, 20 Sep 2025 21:41:14 +0330 Subject: [PATCH 07/11] fix(checker): use TypeFlagsTypeVariable for consistency with getInferenceInfoForType - Change from TypeFlagsTypeParameter to TypeFlagsTypeVariable to match the pattern used in getInferenceInfoForType function - This ensures consistency in type flag checking across inference-related functions - TypeFlagsTypeVariable includes both type parameters and indexed access types, which is more comprehensive - Updated comment to reflect the more accurate terminology 'type variable' instead of 'type parameter' --- internal/checker/inference.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index fecdaa792e..10975d69ff 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1631,9 +1631,9 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn // 3. For each union member, checking if the indexed property type matches the source literal. // 4. If a match is found, inferring that union member as a candidate for the type parameter. func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Type, target *IndexedAccessType) { - // Only proceed if the object type is a type parameter that we're inferring + // Only proceed if the object type is a type variable that we're inferring objectType := target.objectType - if objectType.flags&TypeFlagsTypeParameter != 0 { + if objectType.flags&TypeFlagsTypeVariable != 0 { // Get the inference info for the type parameter inference := getInferenceInfoForType(n, objectType) if inference == nil || inference.isFixed { From e500bd58dc91309486515bb23175d344eab3fe63 Mon Sep 17 00:00:00 2001 From: "arash.mousavi" Date: Sat, 20 Sep 2025 21:45:16 +0330 Subject: [PATCH 08/11] perf(checker): optimize blockedStringType check in union member loop - Move blockedStringType check to beginning of loop for early exit - Avoid expensive getIndexedAccessType() and isTypeIdenticalTo() calls when union member is blocked - Improves performance by skipping unnecessary computations for blocked string types - No functional changes, pure optimization --- internal/checker/inference.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index 10975d69ff..a7217dace8 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1654,6 +1654,14 @@ func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Typ // Look for a union member where the indexed access type matches the source literal indexType := target.indexType for _, unionMember := range constraint.Types() { + // Early check: skip if this union member is the blocked string type + // This type is used as a sentinel to represent cases where string inference should not occur, + // such as when a string index signature would lead to overly broad or incorrect inference. + // Blocking it here avoids unsound or unintended type inference results. + if unionMember == c.blockedStringType { + continue + } + // Try to get the type of the indexed property from this union member memberIndexedType := c.getIndexedAccessType(unionMember, indexType) @@ -1666,13 +1674,6 @@ func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Typ if c.isTypeIdenticalTo(source, memberIndexedType) { // Found a match! Infer this union member as a candidate for the type parameter candidate := unionMember - // Prevent inferring the blocked string type as a candidate. - // This type is used as a sentinel to represent cases where string inference should not occur, - // such as when a string index signature would lead to overly broad or incorrect inference. - // Blocking it here avoids unsound or unintended type inference results. - if candidate == c.blockedStringType { - return - } if n.priority < inference.priority { inference.candidates = nil From 089ee80d035334df4b9cfefbf78a8b52e487b452 Mon Sep 17 00:00:00 2001 From: Arash <75931882+arash-mosavi@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:48:32 +0330 Subject: [PATCH 09/11] Update internal/checker/inference.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/checker/inference.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index a7217dace8..c50467008c 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1654,10 +1654,7 @@ func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Typ // Look for a union member where the indexed access type matches the source literal indexType := target.indexType for _, unionMember := range constraint.Types() { - // Early check: skip if this union member is the blocked string type - // This type is used as a sentinel to represent cases where string inference should not occur, - // such as when a string index signature would lead to overly broad or incorrect inference. - // Blocking it here avoids unsound or unintended type inference results. + // Skip sentinel type used to block string inference if unionMember == c.blockedStringType { continue } From 7dfac8f2a3b7e6405790f7be98d10a02733f3019 Mon Sep 17 00:00:00 2001 From: Arash <75931882+arash-mosavi@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:48:44 +0330 Subject: [PATCH 10/11] Update internal/checker/inference.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/checker/inference.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index c50467008c..bbf4ff33d0 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1613,23 +1613,7 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn // inferFromLiteralToIndexedAccess implements a reverse inference algorithm for indexed access types. // // This function is used during type inference when a literal value is assigned to an indexed access type, -// such as in the pattern `T['type'] = 'Declaration'`. In this scenario, we can infer that the type parameter -// `T` must be a type where the property `'type'` has the value `'Declaration'`. This is particularly useful -// for discriminated union type inference, where the discriminant property (e.g., 'type') determines the -// specific union member. -// -// Example: -// Given a union type: -// type Node = { type: 'Declaration', ... } | { type: 'Expression', ... } -// and an assignment: -// T['type'] = 'Declaration' -// this function infers that T = { type: 'Declaration', ... }. -// -// The algorithm works by: -// 1. Checking if the object type of the indexed access is a type parameter being inferred. -// 2. Looking at the constraint of the type parameter (typically a union). -// 3. For each union member, checking if the indexed property type matches the source literal. -// 4. If a match is found, inferring that union member as a candidate for the type parameter. +// Infers a type parameter from a literal assigned to an indexed access, e.g. T['type'] = 'Declaration'. func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Type, target *IndexedAccessType) { // Only proceed if the object type is a type variable that we're inferring objectType := target.objectType From 57f99f382014be3524ddd02728c2909674f5b1a6 Mon Sep 17 00:00:00 2001 From: Arash <75931882+arash-mosavi@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:49:36 +0330 Subject: [PATCH 11/11] Update internal/checker/inference.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/checker/inference.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/checker/inference.go b/internal/checker/inference.go index bbf4ff33d0..6f29d69354 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1611,9 +1611,8 @@ func (c *Checker) mergeInferences(target []*InferenceInfo, source []*InferenceIn } // inferFromLiteralToIndexedAccess implements a reverse inference algorithm for indexed access types. -// -// This function is used during type inference when a literal value is assigned to an indexed access type, -// Infers a type parameter from a literal assigned to an indexed access, e.g. T['type'] = 'Declaration'. +// This function is used during type inference when a literal value is assigned to an indexed access type. +// It infers a type parameter from a literal assigned to an indexed access, e.g. T['type'] = 'Declaration'. func (c *Checker) inferFromLiteralToIndexedAccess(n *InferenceState, source *Type, target *IndexedAccessType) { // Only proceed if the object type is a type variable that we're inferring objectType := target.objectType