Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b2005d0
quantified types
devanshj Dec 30, 2025
52ac36c
delete temp playground.ts
devanshj Dec 30, 2025
a051f76
redo and do only for object literals
devanshj Jan 4, 2026
62adca2
generalize checking for all expressions not just object literals
devanshj Jan 5, 2026
6318b2f
tweak relater to handle unions
devanshj Jan 6, 2026
ecb3fe3
fix bad test commit
devanshj Jan 6, 2026
13eaee4
preserve parent inference context
devanshj Jan 10, 2026
63729c4
more parent contextual inference
devanshj Jan 10, 2026
499b4eb
add redux toolkit usecase
devanshj Jan 10, 2026
bbe80a6
add #25051 usecase
devanshj Jan 11, 2026
d84379f
will fix infinite recursions in some jsx cases later
devanshj Jan 11, 2026
9ce47b8
rename test and add todo
devanshj Jan 16, 2026
bf09fa5
add failing test for parent inference context
devanshj Jan 16, 2026
5f21027
bind type parameters to variable declarations
devanshj Jan 17, 2026
fdba94b
add minimal simpler test for bounded type parameters
devanshj Jan 17, 2026
daaefa0
way better error messages for bounded type parameters
devanshj Jan 17, 2026
27efc22
fix grammar
devanshj Jan 17, 2026
c803b19
delete correlated union tests case because it doesn't really solve th…
devanshj Jan 18, 2026
7f46918
unwrap baseType at a couple of more places
devanshj Jan 19, 2026
d9b6e8a
remove accidentally committed comments
devanshj Jan 23, 2026
d4f68f1
add an example of correlated union cases that is unsolved
devanshj Jan 24, 2026
d9737f1
fix comment
devanshj Jan 24, 2026
1c5e9f8
remove accidentally added comment
devanshj Jan 24, 2026
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
41 changes: 41 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,8 @@ func (n *Node) TypeParameterList() *NodeList {
return n.AsTypeAliasDeclaration().TypeParameters
case KindJSDocTemplateTag:
return n.AsJSDocTemplateTag().TypeParameters
case KindQuantifiedType:
return n.AsQuantifiedTypeNode().TypeParameters
default:
funcLike := n.FunctionLikeData()
if funcLike != nil {
Expand Down Expand Up @@ -1750,6 +1752,10 @@ func (n *Node) AsConstructorTypeNode() *ConstructorTypeNode {
return n.data.(*ConstructorTypeNode)
}

func (n *Node) AsQuantifiedTypeNode() *QuantifiedTypeNode {
return n.data.(*QuantifiedTypeNode)
}

func (n *Node) AsTypeQueryNode() *TypeQueryNode {
return n.data.(*TypeQueryNode)
}
Expand Down Expand Up @@ -8871,6 +8877,41 @@ func IsTemplateLiteralTypeSpan(node *Node) bool {
return node.Kind == KindTemplateLiteralTypeSpan
}

// QuantifiedTypeNode

type QuantifiedTypeNode struct {
TypeNodeBase
LocalsContainerBase
TypeParameters *NodeList // NodeList[*TypeParameterDeclarationNode]
BaseType *TypeNode
}

func (f *NodeFactory) NewQuantifiedTypeNode(typeParameters *NodeList, baseTypeNode *TypeNode) *Node {
data := &QuantifiedTypeNode{}
data.TypeParameters = typeParameters
data.BaseType = baseTypeNode
return f.newNode(KindQuantifiedType, data)
}

func (f *NodeFactory) UpdateQuantifiedTypeNode(node *QuantifiedTypeNode, typeParameters *NodeList, baseTypeNode *TypeNode) *Node {
if typeParameters != node.TypeParameters || baseTypeNode != node.BaseType {
return updateNode(f.NewQuantifiedTypeNode(typeParameters, baseTypeNode), node.AsNode(), f.hooks)
}
return node.AsNode()
}

func (node *QuantifiedTypeNode) ForEachChild(v Visitor) bool {
return visitNodeList(v, node.TypeParameters) || visit(v, node.BaseType)
}

func (node *QuantifiedTypeNode) VisitEachChild(v *NodeVisitor) *Node {
return v.Factory.UpdateQuantifiedTypeNode(node, v.visitNodes(node.TypeParameters), v.visitNode(node.BaseType))
}

func (node *QuantifiedTypeNode) Clone(f NodeFactoryCoercible) *Node {
return cloneNode(f.AsNodeFactory().NewQuantifiedTypeNode(node.TypeParameters, node.BaseType), node.AsNode(), f.AsNodeFactory().hooks)
}

// SyntheticExpression

type SyntheticExpression struct {
Expand Down
1 change: 1 addition & 0 deletions internal/ast/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ const (
KindNamedTupleMember
KindTemplateLiteralType
KindTemplateLiteralTypeSpan
KindQuantifiedType
KindImportType
// Binding patterns
KindObjectBindingPattern
Expand Down
303 changes: 152 additions & 151 deletions internal/ast/kind_stringer_generated.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion internal/ast/precedence.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,8 @@ const (
// Gets the precedence of a TypeNode
func GetTypeNodePrecedence(n *TypeNode) TypePrecedence {
switch n.Kind {
case KindConditionalType:
case KindConditionalType,
KindQuantifiedType:
return TypePrecedenceConditional
case KindJSDocOptionalType, KindJSDocVariadicType:
return TypePrecedenceJSDoc
Expand Down
5 changes: 3 additions & 2 deletions internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,8 @@ func (b *Binder) declareSymbolAndAddToSymbolTable(node *ast.Node, symbolFlags as
case ast.KindFunctionType, ast.KindConstructorType, ast.KindCallSignature, ast.KindConstructSignature,
ast.KindIndexSignature, ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindConstructor, ast.KindGetAccessor,
ast.KindSetAccessor, ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction,
ast.KindClassStaticBlockDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindMappedType:
ast.KindClassStaticBlockDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindMappedType,
ast.KindQuantifiedType:
return b.declareSymbol(ast.GetLocals(b.container), nil /*parent*/, node, symbolFlags, symbolExcludes)
}
panic("Unhandled case in declareSymbolAndAddToSymbolTable")
Expand Down Expand Up @@ -2467,7 +2468,7 @@ func GetContainerFlags(node *ast.Node) ContainerFlags {
return ContainerFlagsIsContainer
case ast.KindInterfaceDeclaration:
return ContainerFlagsIsContainer | ContainerFlagsIsInterface
case ast.KindModuleDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindMappedType, ast.KindIndexSignature:
case ast.KindModuleDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindMappedType, ast.KindIndexSignature, ast.KindQuantifiedType:
return ContainerFlagsIsContainer | ContainerFlagsHasLocals
case ast.KindSourceFile:
return ContainerFlagsIsContainer | ContainerFlagsIsControlFlowContainer | ContainerFlagsHasLocals
Expand Down
173 changes: 158 additions & 15 deletions internal/checker/checker.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion internal/checker/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (c *Checker) HasEffectiveRestParameter(signature *Signature) bool {
}

func (c *Checker) GetLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol *ast.Symbol) []*Type {
return c.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol)
return c.getLocalTypeParametersOfClassOrInterfaceOrTypeAliasOrQuantifiedType(symbol)
}

func (c *Checker) GetContextualTypeForObjectLiteralElement(element *ast.Node, contextFlags ContextFlags) *Type {
Expand Down
2 changes: 1 addition & 1 deletion internal/checker/jsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ func (c *Checker) getJsxPropsTypeFromClassType(sig *Signature, context *ast.Node
apparentAttributesType := attributesType
intrinsicClassAttribs := c.getJsxType(JsxNames.IntrinsicClassAttributes, context)
if !c.isErrorType(intrinsicClassAttribs) {
typeParams := c.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol)
typeParams := c.getLocalTypeParametersOfClassOrInterfaceOrTypeAliasOrQuantifiedType(intrinsicClassAttribs.symbol)
hostClassType := c.getReturnTypeOfSignature(sig)
var libraryManagedAttributeType *Type
if typeParams != nil {
Expand Down
14 changes: 12 additions & 2 deletions internal/checker/nodebuilderimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ func (b *NodeBuilderImpl) getNameOfSymbolAsWritten(symbol *ast.Symbol) string {
func (b *NodeBuilderImpl) getTypeParametersOfClassOrInterface(symbol *ast.Symbol) []*Type {
result := make([]*Type, 0)
result = append(result, b.ch.getOuterTypeParametersOfClassOrInterface(symbol)...)
result = append(result, b.ch.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol)...)
result = append(result, b.ch.getLocalTypeParametersOfClassOrInterfaceOrTypeAliasOrQuantifiedType(symbol)...)
return result
}

Expand Down Expand Up @@ -1518,7 +1518,7 @@ func (b *NodeBuilderImpl) typeParametersToTypeParameterDeclarations(symbol *ast.
targetSymbol := b.ch.getTargetSymbol(symbol)
if targetSymbol.Flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface|ast.SymbolFlagsAlias) != 0 {
var results []*ast.Node
params := b.ch.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol)
params := b.ch.getLocalTypeParametersOfClassOrInterfaceOrTypeAliasOrQuantifiedType(symbol)
for _, param := range params {
results = append(results, b.typeParameterToDeclaration(param))
}
Expand Down Expand Up @@ -3091,6 +3091,16 @@ func (b *NodeBuilderImpl) typeToTypeNode(t *Type) *ast.TypeNode {
return typeNode
}
}
if t.flags&TypeFlagsQuantified != 0 {
return b.f.NewQuantifiedTypeNode(
b.f.NewNodeList(
core.Map(t.AsQuantifiedType().typeParameters, func(typeParamter *TypeParameter) *ast.Node {
return b.typeParameterToDeclaration(typeParamter.AsType())
}),
),
b.typeToTypeNode(t.AsQuantifiedType().baseType),
)
}

panic("Should be unreachable.")
}
Expand Down
71 changes: 65 additions & 6 deletions internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,31 @@ func (c *Checker) areTypesComparable(type1 *Type, type2 *Type) bool {
}

func (c *Checker) isTypeRelatedTo(source *Type, target *Type, relation *Relation) bool {
if target.flags&TypeFlagsQuantified != 0 {
if source.flags&TypeFlagsQuantified != 0 {
return source == target
}
baseType := target.AsQuantifiedType().baseType
typeParameters := core.Map(target.AsQuantifiedType().typeParameters, func(t *TypeParameter) *Type { return t.AsType() })

if source.flags&TypeFlagsUnion == 0 {
inferenceContext := c.newInferenceContext(typeParameters, nil, InferenceFlagsNone, nil)
c.inferTypes(inferenceContext.inferences, source, baseType, InferencePriorityNone, false)
return c.isTypeRelatedTo(source, c.instantiateType(baseType, inferenceContext.mapper), relation)
}

for _, sourceMember := range source.AsUnionType().types {
inferenceContext := c.newInferenceContext(typeParameters, nil, InferenceFlagsNone, nil)
c.inferTypes(inferenceContext.inferences, sourceMember, baseType, InferencePriorityNone, false)
result := c.isTypeRelatedTo(sourceMember, c.instantiateType(baseType, inferenceContext.mapper), relation)
if result {
continue
}
return result
}

return true
}
if isFreshLiteralType(source) {
source = source.AsLiteralType().regularType
}
Expand Down Expand Up @@ -2553,6 +2578,32 @@ func (r *Relater) isRelatedTo(source *Type, target *Type, recursionFlags Recursi
}

func (r *Relater) isRelatedToEx(originalSource *Type, originalTarget *Type, recursionFlags RecursionFlags, reportErrors bool, headMessage *diagnostics.Message, intersectionState IntersectionState) Ternary {
if originalTarget.flags&TypeFlagsQuantified != 0 {
if originalSource.flags&TypeFlagsQuantified != 0 && originalSource == originalTarget {
return TernaryTrue
}
baseType := originalTarget.AsQuantifiedType().baseType
typeParameters := core.Map(originalTarget.AsQuantifiedType().typeParameters, func(t *TypeParameter) *Type { return t.AsType() })

if originalSource.flags&TypeFlagsUnion == 0 {
inferenceContext := r.c.newInferenceContext(typeParameters, nil, InferenceFlagsNone, nil)
r.c.inferTypes(inferenceContext.inferences, originalSource, baseType, InferencePriorityNone, false)
return r.isRelatedToEx(originalSource, r.c.instantiateType(baseType, inferenceContext.mapper), recursionFlags, reportErrors, headMessage, intersectionState)
}

for _, originalSourceMember := range originalSource.AsUnionType().types {
inferenceContext := r.c.newInferenceContext(typeParameters, nil, InferenceFlagsNone, nil)
r.c.inferTypes(inferenceContext.inferences, originalSourceMember, baseType, InferencePriorityNone, false)
result := r.isRelatedToEx(originalSourceMember, r.c.instantiateType(baseType, inferenceContext.mapper), recursionFlags, reportErrors, headMessage, intersectionState)
if result == TernaryTrue {
continue
}
return result
}

return TernaryTrue
}

if originalSource == originalTarget {
return TernaryTrue
}
Expand Down Expand Up @@ -4672,12 +4723,17 @@ func (r *Relater) reportErrorResults(originalSource *Type, originalTarget *Type,
}
}
r.reportRelationError(headMessage, source, target)
if source.flags&TypeFlagsTypeParameter != 0 && source.symbol != nil && len(source.symbol.Declarations) != 0 && r.c.getConstraintOfType(source) == nil {
syntheticParam := r.c.cloneTypeParameter(source)
syntheticParam.AsTypeParameter().constraint = r.c.instantiateType(target, newSimpleTypeMapper(source, syntheticParam))
if r.c.hasNonCircularBaseConstraint(syntheticParam) {
targetConstraintString := r.c.TypeToString(target)
r.relatedInfo = append(r.relatedInfo, NewDiagnosticForNode(source.symbol.Declarations[0], diagnostics.This_type_parameter_might_need_an_extends_0_constraint, targetConstraintString))
if source.objectFlags&ObjectFlagsQuantifiedTypeParameter != 0 && target.objectFlags&ObjectFlagsQuantifiedTypeParameter != 0 {
r.relatedInfo = append(r.relatedInfo, NewDiagnosticForNode(source.AsTypeParameter().boundedTo.Declarations[0], diagnostics.Type_parameter_0_is_bounded_to_this_variable, r.c.TypeToString(source)))
r.relatedInfo = append(r.relatedInfo, NewDiagnosticForNode(target.AsTypeParameter().boundedTo.Declarations[0], diagnostics.Type_parameter_0_is_bounded_to_this_variable, r.c.TypeToString(target)))
} else {
if source.flags&TypeFlagsTypeParameter != 0 && source.symbol != nil && len(source.symbol.Declarations) != 0 && r.c.getConstraintOfType(source) == nil {
syntheticParam := r.c.cloneTypeParameter(source)
syntheticParam.AsTypeParameter().constraint = r.c.instantiateType(target, newSimpleTypeMapper(source, syntheticParam))
if r.c.hasNonCircularBaseConstraint(syntheticParam) {
targetConstraintString := r.c.TypeToString(target)
r.relatedInfo = append(r.relatedInfo, NewDiagnosticForNode(source.symbol.Declarations[0], diagnostics.This_type_parameter_might_need_an_extends_0_constraint, targetConstraintString))
}
}
}
}
Expand Down Expand Up @@ -4712,6 +4768,9 @@ func (r *Relater) reportRelationError(message *diagnostics.Message, source *Type
r.reportError(diagnostics.X_0_could_be_instantiated_with_an_arbitrary_type_which_could_be_unrelated_to_1, targetType, generalizedSourceType)
}
}
if target.objectFlags&ObjectFlagsQuantifiedTypeParameter != 0 && generalizedSource.objectFlags&ObjectFlagsQuantifiedTypeParameter != 0 {
r.reportError(diagnostics.Both_type_parameters_are_bounded_to_different_variables)
}
if message == nil {
switch {
case r.relation == r.c.comparableRelation:
Expand Down
17 changes: 14 additions & 3 deletions internal/checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ type SignatureLinks struct {
decoratorSignature *Signature // Signature for decorator as if invoked by the runtime
}

type TypeFlags uint32
type TypeFlags uint64

// Note that for types of different kinds, the numeric values of TypeFlags determine the order
// computed by the CompareTypes function and therefore the order of constituent types in union types.
Expand Down Expand Up @@ -421,6 +421,7 @@ const (
TypeFlagsReserved1 TypeFlags = 1 << 29 // Used by union/intersection type construction
TypeFlagsReserved2 TypeFlags = 1 << 30 // Used by union/intersection type construction
TypeFlagsReserved3 TypeFlags = 1 << 31
TypeFlagsQuantified TypeFlags = 1 << 32

TypeFlagsAnyOrUnknown = TypeFlagsAny | TypeFlagsUnknown
TypeFlagsNullable = TypeFlagsUndefined | TypeFlagsNull
Expand All @@ -445,7 +446,7 @@ const (
TypeFlagsUnionOrIntersection = TypeFlagsUnion | TypeFlagsIntersection
TypeFlagsStructuredType = TypeFlagsObject | TypeFlagsUnion | TypeFlagsIntersection
TypeFlagsTypeVariable = TypeFlagsTypeParameter | TypeFlagsIndexedAccess
TypeFlagsInstantiableNonPrimitive = TypeFlagsTypeVariable | TypeFlagsConditional | TypeFlagsSubstitution
TypeFlagsInstantiableNonPrimitive = TypeFlagsTypeVariable | TypeFlagsConditional | TypeFlagsSubstitution | TypeFlagsQuantified
TypeFlagsInstantiablePrimitive = TypeFlagsIndex | TypeFlagsTemplateLiteral | TypeFlagsStringMapping
TypeFlagsInstantiable = TypeFlagsInstantiableNonPrimitive | TypeFlagsInstantiablePrimitive
TypeFlagsStructuredOrInstantiable = TypeFlagsStructuredType | TypeFlagsInstantiable
Expand Down Expand Up @@ -497,10 +498,12 @@ const (
ObjectFlagsCouldContainTypeVariablesComputed ObjectFlags = 1 << 19 // CouldContainTypeVariables flag has been computed
ObjectFlagsCouldContainTypeVariables ObjectFlags = 1 << 20 // Type could contain a type variable
ObjectFlagsMembersResolved ObjectFlags = 1 << 21 // Members have been resolved
ObjectFlagsContainsQuantifiedType ObjectFlags = 1 << 22
ObjectFlagsQuantifiedTypeParameter ObjectFlags = 1 << 23

ObjectFlagsClassOrInterface = ObjectFlagsClass | ObjectFlagsInterface
ObjectFlagsRequiresWidening = ObjectFlagsContainsWideningType | ObjectFlagsContainsObjectOrArrayLiteral
ObjectFlagsPropagatingFlags = ObjectFlagsContainsWideningType | ObjectFlagsContainsObjectOrArrayLiteral | ObjectFlagsNonInferrableType
ObjectFlagsPropagatingFlags = ObjectFlagsContainsWideningType | ObjectFlagsContainsObjectOrArrayLiteral | ObjectFlagsNonInferrableType | ObjectFlagsContainsQuantifiedType
ObjectFlagsInstantiatedMapped = ObjectFlagsMapped | ObjectFlagsInstantiated
// Object flags that uniquely identify the kind of ObjectType
ObjectFlagsObjectTypeKindMask = ObjectFlagsClassOrInterface | ObjectFlagsReference | ObjectFlagsTuple | ObjectFlagsAnonymous | ObjectFlagsMapped | ObjectFlagsReverseMapped | ObjectFlagsEvolvingArray | ObjectFlagsInstantiationExpressionType | ObjectFlagsSingleSignatureType
Expand Down Expand Up @@ -594,6 +597,7 @@ func (t *Type) AsTemplateLiteralType() *TemplateLiteralType { return t.data.(*Te
func (t *Type) AsStringMappingType() *StringMappingType { return t.data.(*StringMappingType) }
func (t *Type) AsSubstitutionType() *SubstitutionType { return t.data.(*SubstitutionType) }
func (t *Type) AsConditionalType() *ConditionalType { return t.data.(*ConditionalType) }
func (t *Type) AsQuantifiedType() *QuantifiedType { return t.data.(*QuantifiedType) }

// Casts for embedded struct types

Expand Down Expand Up @@ -1028,6 +1032,7 @@ type TypeParameter struct {
mapper *TypeMapper
isThisType bool
resolvedDefaultType *Type
boundedTo *ast.Symbol
}

// IndexFlags
Expand Down Expand Up @@ -1100,6 +1105,12 @@ type ConditionalType struct {
combinedMapper *TypeMapper
}

type QuantifiedType struct {
ConstrainedType
typeParameters []*TypeParameter
baseType *Type
}

// SignatureFlags

type SignatureFlags uint32
Expand Down
8 changes: 8 additions & 0 deletions internal/diagnostics/diagnostics_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions internal/diagnostics/extraDiagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,13 @@
"Option '{0}' requires value to be greater than '{1}'.": {
"category": "Error",
"code": 5002
},
"Both type parameters are bounded to different variables": {
"category": "Error",
"code": 5113
},
"Type parameter '{0}' is bounded to this variable": {
"category": "Error",
"code": 5114
}
}
15 changes: 15 additions & 0 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2480,6 +2480,21 @@ func (p *Parser) parseType() *ast.TypeNode {
saveContextFlags := p.contextFlags
p.setContextFlags(ast.NodeFlagsTypeExcludesFlags, false)
var typeNode *ast.TypeNode
if p.token == ast.KindLessThanToken {
state := p.mark()
p.parseFunctionOrConstructorType()
couldParseFunctionType := len(p.diagnostics) == state.diagnosticsLen
// TODO: see if there's a more standard way to do "try" parse
p.rewind(state)
if !couldParseFunctionType {
pos := p.nodePos()
typeParameters := p.parseTypeParameters()
baseType := p.parseType()
typeNode = p.finishNode(p.factory.NewQuantifiedTypeNode(typeParameters, baseType), pos)
p.contextFlags = saveContextFlags
return typeNode
}
}
if p.isStartOfFunctionTypeOrConstructorType() {
typeNode = p.parseFunctionOrConstructorType()
} else {
Expand Down
Loading