Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
68 changes: 68 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,67 @@ 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 {
// 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
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