Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions internal/checker/inference.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1605,3 +1609,88 @@ 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.
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 {
// 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 := 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
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
}
}
}
}
72 changes: 72 additions & 0 deletions testdata/baselines/reference/compiler/cssTreeTypeInference.js
Original file line number Diff line number Diff line change
@@ -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<T extends ASTNode> {
visit: T['type'];
enter(node: T): void;
}

declare function walk<T extends ASTNode>(ast: ASTNode, options: WalkOptions<T>): 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
},
});
128 changes: 128 additions & 0 deletions testdata/baselines/reference/compiler/cssTreeTypeInference.symbols
Original file line number Diff line number Diff line change
@@ -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<T extends ASTNode> {
>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<T extends ASTNode>(ast: ASTNode, options: WalkOptions<T>): 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))

},
});
Loading