@@ -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