New solver fails to solve type pack with generic vararg
Consider a wrapper around RemoteEvents to provide greater type support (ensure the server sends the correct data types to the client). One might try write:
--!strict
type Player = {__type: "PLAYER"} -- patched for native Luau
type TypedEvent<T...> = {
Connect: (self: TypedEvent<T...>, cb: (T...) -> ()) -> RBXScriptConnection,
}
type TypedRemoteEvent<T...> = {
FireServer: (self: TypedRemoteEvent<T...>, T...) -> (),
FireAllClients: (self: TypedRemoteEvent<T...>, T...) -> (),
FireClient: (self: TypedRemoteEvent<T...>, player: Player, T...) -> (),
OnClientEvent: TypedEvent<T...>,
OnServerEvent: TypedEvent<(Player, ...unknown)>
}
function connectRemote<T...>(remote: TypedRemoteEvent<T...>, callback: (Player, ...unknown) -> ())
remote.OnServerEvent:Connect(function(player, ...)
callback(player, ...)
end)
end
local remotes = {
Debug = {
-- set to nil because Luau doesn't have Roblox instances. In Roblox this would refer to a RemoteEvent Instance
TestRemote = (nil :: any) :: TypedRemoteEvent<number, boolean>,
}
}
connectRemote(remotes.Debug.TestRemote, function(player, a, b) -- errors!
return
end)
however this yields many type errors in the new solver:
TypeError: Type pack 'number, boolean' could not be converted into 'T...'
TypeError: Type pack 'number, boolean' could not be converted into 'T...'
TypeError: Type pack 'number, boolean' could not be converted into 'T...'
TypeError: Type pack 'number, boolean' could not be converted into 'T...'
TypeError: Type pack 'TypedRemoteEvent<number, boolean>, Player, number, boolean' could not be converted into 'T...'
TypeError: Type pack 'TypedRemoteEvent<number, boolean>, Player, number, boolean' could not be converted into 'T...'
these errors should not exist. Additionally, both a and b should solve as unknown.
The type solver seems to struggle to understand what remotes.Debug.TestRemote should solve as. Hovering over it produces
t3 where t1 = {
Connect: (self: t1, cb: ({
__type: "PLAYER"
}, ...unknown) -> ()) -> RBXScriptConnection
} ; t2 = {
Connect: (self: t2, cb: (number, boolean) -> ()) -> RBXScriptConnection
} ; t3 = {
FireAllClients: (self: t3, number, boolean) -> (),
... 4 more ...
}
Notably if you reduce the definition of TypedRemoteEvent down to simply
type TypedRemoteEvent<T...> = {
OnClientEvent: TypedEvent<T...>,
OnServerEvent: TypedEvent<(Player, ...unknown)>
}
then there is no errors. It is the introduction of FireClient or FireAllClients or FireServer that causes the errors to appear.
Even stranger: if you reduce TypedRemoteEvent down to
type TypedRemoteEvent<T...> = {
FireClient: (player: Player, T...) -> (),
FireAllClients: (T...) -> (),
FireServer: (T...) -> (),
OnClientEvent: TypedEvent<T...>,
OnServerEvent: TypedEvent<(Player, ...unknown)>
}
(making it incorrect by excluding self to make it a method rather than dot notation call) you still get one error:
TypeError: Type pack 'Player, number, boolean' could not be converted into 'T...'
which is caused by the FireClient definition. Removing the FireClient definition results in no errors again.
I thought I'd chip in with a further simplification of the issue in case it might prove useful to the Luau team.
Notably if you reduce the definition of
TypedRemoteEventdown to simplytype TypedRemoteEvent<T...> = { OnClientEvent: TypedEvent<T...>, OnServerEvent: TypedEvent<(Player, ...unknown)> }
then there is no errors. It is the introduction of
FireClientorFireAllClientsorFireServerthat causes the errors to appear.
As you noticed here, FireClient/FireAllClients/FireServer cause the issue[s]. We can simplify the type like so:
type foo = { bar: < T... >( any, T... ) -> T... }
local baz = ({} :: foo):bar("Hello world!") -- baz resolves as `unknown` when it should resolve as `string`
The issue lies specifically in the fact that there exists parameters 'before' the generic pack. Naturally, the problem solves itself if we remove the leading parameters:
type foo = { bar: < T... >( T... ) -> T... }
local baz = ({} :: foo).bar("Hello world!") -- baz appropriately resolves as `string`
-- or, to demonstrate that the issue is not somehow the result of method call sugar:
local baz = ({} :: foo):bar() -- baz appropriately resolves as `foo`
This is why, in this simplification, you noticed that FireClient was the only erroneous member: because it has a parameter which precedes a pack of generics.
It might be worth noting that this issue does not happen with individual generics.
type foo = { bar: < T >( any, T ) -> T }
local baz = ({} :: foo):bar("Hello world!") -- baz again appropriately resolves as `string`
I just tried this repro again, and it looks like it's working now? Strangely I tried with a super old luau-analyze and it also worked, so not sure what magic is going on there.