Repository navigation
Description
🔎 Search Terms
"tsserver", "generic object completion", "generic intellisense", "generic typeahead", "generic completion", "generic object properties", "object property completion", "typescript server", "typescript lsp"
🕗 Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about typeahead support for generic type arguments.
⏯ Playground Link
💻 Code
type TypeA<T extends 'one' | 'two'> = T
type TypeB<T extends { prop: 'one' | 'two' }> = T
// ✅ Good: Completions provided
type ExampleA = TypeA<''>
// ❌ Bad: Completions NOT provided
type ExampleB = TypeB<{ prop: '' }>🙁 Actual behavior
Getting completions between the quotes on line 5 (using Control + Space on Mac OS) correctly provides 'one' and 'two' as options.
However, doing the same between the quotes on line 8 provides no completions.
🙂 Expected behavior
Getting completions between the quotes on both lines 5 and 8 (using Control + Space on Mac OS) correctly provides 'one' and 'two' as options.
Additional information about the issue
The type of prop is correct ('one' | 'two') and if you enter an invalid value you get the correct type error. Therefore, TypeScript should have sufficient information to provide the exact same completions in both examples.
I enabled verbose tsserver logging in Visual Studio Code and captured the requests and responses for these completion requests.
Line 5 Request:
{
"seq":12,
"type":"request",
"command":"completionInfo",
"arguments":{
"file":"/Users/ryan.palmer/Desktop/Test.ts",
"line":5,
"offset":24,
"includeExternalModuleExports":true,
"includeInsertTextCompletions":true,
"triggerKind":1
}
}Line 5 Response:
{
"seq":0,
"type":"response",
"command":"completionInfo",
"request_seq":12,
"success":true,
"body":{
"isGlobalCompletion":false,
"isMemberCompletion":false,
"isNewIdentifierLocation":false,
"optionalReplacementSpan":{
"start":{
"line":5,
"offset":24
},
"end":{
"line":5,
"offset":24
}
},
"entries":[
{
"name":"one",
"kind":"string",
"kindModifiers":"",
"sortText":"11",
"replacementSpan":{
"start":{
"line":5,
"offset":24
},
"end":{
"line":5,
"offset":24
}
}
},
{
"name":"two",
"kind":"string",
"kindModifiers":"",
"sortText":"11",
"replacementSpan":{
"start":{
"line":5,
"offset":24
},
"end":{
"line":5,
"offset":24
}
}
}
]
}
}Line 8 Request:
{
"seq":9,
"type":"request",
"command":"completionInfo",
"arguments":{
"file":"/Users/ryan.palmer/Desktop/Test.ts",
"line":8,
"offset":32,
"includeExternalModuleExports":true,
"includeInsertTextCompletions":true,
"triggerKind":1
}
}Line 8 Response:
{
"seq":0,
"type":"response",
"command":"completionInfo",
"request_seq":9,
"success":false,
"message":"No content available."
}Line 8 Logs:
Info 451 [17:32:09.801] getCompletionData: Get current token: 0.018829017877578735
Info 452 [17:32:09.801] getCompletionData: Is inside comment: 0.007900983095169067
Info 453 [17:32:09.801] getCompletionData: Get previous token: 0.10709202289581299
Info 454 [17:32:09.802] getCompletionsAtPosition: isCompletionListBlocker: 0.03820300102233887
Info 455 [17:32:09.802] Returning an empty list because completion was requested in an invalid position.
As you can see, the requests are both identical except for position. But the response for the request on line 8 is empty, and there is a message indicating the position is invalid.
Activity
Andarist commentedon Nov 6, 2023
It looks like a strong candidate for being a duplicate of #52898
kronodeus commentedon Nov 7, 2023
Mateusz Burzyński (@Andarist) The issue described there seems to be a much more severe lack of completions for type arguments to functions. However, it is specific to functions. The issue described there does not apply to type references, which my issue relates to.
Here is a simplified example that demonstrates that functions get no completions at all for type arguments while type references do:
That issue actually seems a lot more straightforward to fix because I can't think of a reason why you couldn't route completion requests for type arguments to functions down the same code path as type arguments to type references. But maybe that's overly optimistic.
kronodeus commentedon Nov 7, 2023
The core problem starts here in
stringCompletions.ts. When getting completions for the example code, the grandparent node of the string literal node isSyntaxKind.PropertySignature(AST viewer link), but the switch statement infromUnionableLiteralTypedoesn't have a case for this.Intuitively, we'd want to handle this the same way we handle the case for
SyntaxKind.TypeReference:However, it doesn't seem trivial because the type checker is also not expecting this node hierarchy. Here in
checker.tsthegetTypeArgumentConstraintfunction is expecting the parent to be aTypeReference. I believe what we would need to do is get the constraint for the actualTypeReferencenode in this context which, given the example code, would be the great-great-grandparent. This constraint would correspond to the whole object, so we'd need to derive a sub-constraint for the string property inside the object.The type checker is clearly already aware of the constraints of the string property because it is correctly showing
"one" | "two"as the type on hover. So this idea of sub-constraints for properties should be achievable with the existing type information. A general-purpose solution is probably needed here to handle other scenarios like functions or nested objects. In essence, instead of assuming the grandparent will be aTypeReference, we walk up the ancestry until we find aTypeReference, get the constraint for that node, and then derive a sub-constraint for node we started from.kronodeus commentedon Nov 9, 2023
I'd like to attempt a fix but could use some guidance. In the meantime, I hacked together a
tsserverplugin that implements completions for this scenario:For now it'll do as a workaround but I don't feel great about this as long-term solution. Some of the internal APIs in the
tsserverlibrary would help here but are evidently not publicly accessible.Edit:
I updated the plugin code above. It now supports a lot more usage scenarios and works for everything I tested.
Andarist commentedon Nov 9, 2023
If u are interested in contributing, i can offer some guidance around the codebase etc. Feel free to DM me
kronodeus commentedon Nov 10, 2023
Mateusz Burzyński (@Andarist) Yes I would greatly appreciate that! I will reach out. I updated the plugin code above in an attempt to achieve the idea I had about deriving sub-constraints for nested type literals. It works great, but I had to utilize some internal APIs to achieve it.
Here's the critical code section:
It recursively walks up to the nearest
TypeReferenceancestor and gets the top-level constraint on the type argument. The sub-constraints then propagate down through the property signatures where they are needed.When I find time I'd love to take a stab at a permanent fix, but does this seem like generally the right direction or am I way off?