luau icon indicating copy to clipboard operation
luau copied to clipboard

New solver fails to solve type pack with generic vararg

Open OverHash opened this issue 9 months ago • 3 comments

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 ...
}

OverHash avatar Mar 29 '25 04:03 OverHash

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.

OverHash avatar Mar 29 '25 04:03 OverHash

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.

OverHash avatar Mar 29 '25 04:03 OverHash

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

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`

ishtar112 avatar Apr 01 '25 06:04 ishtar112

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.

OverHash avatar Sep 21 '25 06:09 OverHash