Skip to content

Commit e35d399

Browse files
committed
feat: enhance type widening for suggestions in generic parameters
1 parent e509603 commit e35d399

1 file changed

Lines changed: 46 additions & 8 deletions

File tree

src/schema/getSignatureSchema.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ const generateNodeSchemas = (
335335
// setting a boolean literal in a generic slot would silently hide all
336336
// other suggestions.
337337
const suggestionType = functionParameterType
338-
? widenForSuggestions(checker, functionParameterType)
338+
? widenForSuggestions(checker, functionParameterType, node!)
339339
: undefined
340340

341341
const nodeSchema = getSchema(
@@ -375,13 +375,51 @@ const generateNodeSchemas = (
375375
// Widen a function parameter type so that suggestion collection asks "what could
376376
// the function accept here", not "what does the current value narrow this to".
377377
// An unconstrained type parameter accepts anything → `any`. A constrained type
378-
// parameter is replaced by its constraint. Everything else is used as-is.
379-
const widenForSuggestions = (checker: ts.TypeChecker, type: ts.Type): ts.Type => {
380-
if ((type.flags & ts.TypeFlags.TypeParameter) === 0) return type
381-
const decl = type.symbol?.declarations?.[0]
382-
if (decl && ts.isTypeParameterDeclaration(decl) && decl.constraint) {
383-
return checker.getTypeFromTypeNode(decl.constraint)
378+
// parameter is replaced by its constraint. Any generic type with free type parameters
379+
// (e.g. LIST<T>, OBJECT<T>) is widened by substituting `any` for each free
380+
// TypeParameter, because TypeScript's isTypeAssignableTo returns false for concrete
381+
// types against generics with free T even though they are structurally valid candidates.
382+
//
383+
// For array types (TypeReference) the public createArrayType API rebuilds the widened
384+
// type directly. For type-alias types (e.g. mapped types like OBJECT<T>) the source
385+
// code is pre-seeded with `declare const __widen_<Name>: <Name><any, …>` declarations
386+
// so the widened type can be looked up in the checker's scope via node.
387+
const widenForSuggestions = (checker: ts.TypeChecker, type: ts.Type, node: ts.VariableDeclaration): ts.Type => {
388+
if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) {
389+
const decl = type.symbol?.declarations?.[0]
390+
if (decl && ts.isTypeParameterDeclaration(decl) && decl.constraint) {
391+
return checker.getTypeFromTypeNode(decl.constraint)
392+
}
393+
return checker.getAnyType()
394+
}
395+
396+
if ((type.flags & ts.TypeFlags.Object) !== 0 && hasFreeTypeParam(type, checker, new Set())) {
397+
const aliasName: string | undefined = (type as any).aliasSymbol?.getName()
398+
if (aliasName) {
399+
const widenedSym = checker
400+
.getSymbolsInScope(node, ts.SymbolFlags.Variable)
401+
.find(s => s.getName() === `__widen_${aliasName}`)
402+
if (widenedSym) {
403+
return checker.getTypeOfSymbolAtLocation(widenedSym, node)
404+
}
405+
}
406+
return checker.getAnyType()
407+
}
408+
409+
return type
410+
}
411+
412+
// Returns true if type itself or any of its type arguments (direct or alias) is a
413+
// free TypeParameter, recursing into nested generic types.
414+
const hasFreeTypeParam = (type: ts.Type, checker: ts.TypeChecker, visited: Set<ts.Type>): boolean => {
415+
if (visited.has(type)) return false
416+
visited.add(type)
417+
if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) return true
418+
if ((type.flags & ts.TypeFlags.Object) !== 0) {
419+
if (checker.getTypeArguments(type as ts.TypeReference).some(arg => hasFreeTypeParam(arg, checker, visited))) return true
420+
const aliasArgs = (type as any).aliasTypeArguments as ts.Type[] | undefined
421+
if (aliasArgs?.some(arg => hasFreeTypeParam(arg, checker, visited))) return true
384422
}
385-
return checker.getAnyType()
423+
return false
386424
}
387425

0 commit comments

Comments
 (0)