Luau: Type 'RBXScriptSignal' could not be converted into 'RBXScriptSignal'

Issue Type: Other
Impact: Moderate
Frequency: Constantly
Date First Experienced: 2021-05-07 17:05:00 (-07:00)
Date Last Experienced: 2021-05-10 11:05:00 (-07:00)

Reproduction Steps:

  • Try to write a function and type it so that it should return a RBXScriptSignal and then try to return an instance of RBXScriptSignal.
  • Try to write a table of type x where said type requires a field of RBXScriptSignal.
  • Try to bind a RBXScriptSignal to a variable of type RBXScriptSignal.

The source is here:

--!strict
type foo = {
	mySignal: RBXScriptSignal
}

function createNewFoo(): foo
	local newFoo = {};
	local fooEvent = Instance.new("BindableEvent");
	
	newFoo.mySignal = fooEvent.Event;
	return newFoo;
	end

local foobar: {RBXScriptSignal} = {
	Instance.new("BindableEvent").Event;
}

local x: RBXScriptSignal = Instance.new("BindableEvent").Event;

Expected Behavior:
I expect the checker to not throw a fit and say that it can’t convert RBXScriptSignal to a RBXScriptSignal.

Actual Behavior:
This happens.
image
image
image

Workaround:
The solution is actually do this:

type RBXScriptSignal = typeof(Instance.new("BindableEvent").Event);

and tag it somewhere at the top of your script. Doing so gives you a beautiful, underline-less environment like so:

11 Likes

It seems that BindableEvent has a very weird type for the Event property, using this I was able to glean some information about the type:

--!strict
type RBX = RBXScriptSignal|typeof(Instance.new"BindableEvent".Event)
local a:RBX = nil::any
a:Connect(function()end) -- W000: (4,1) Cannot call non-function ((RBXScriptSignal, (...any) -> ()) -> RBXScriptConnection) | ((RBXScriptSignal, (any) -> nil) -> RBXScriptConnection)
a:Wait() -- W000: (5,1) Cannot call non-function ((RBXScriptSignal) -> (...any)) | ((RBXScriptSignal) -> any)

It appears that the type of RBXScriptSignal and typeof(Instance.new"BindableEvent".Event) are different types entirely. RBXScriptSignal’s Connect expects a function which takes any number of arguments of any types, and ignores the results. typeof(Instance.new"BindableEvent".Event)’ Connect expects a function which takes one or zero arguments of any type, and returns nothing or nil. RBXScriptSignal’s Wait returns any number of values of any types, while typeof(Instance.new"BindableEvent".Event)'s Wait returns one value of any type. It also appears that typeof(Instance.new"BindableEvent".Event) recognizes ConnectParallel, while RBXScriptSignal doesn’t.

Essentially, it appears that BindableEvent.Event isn’t a RBXScriptSignal, but is actually a unique type.

type TypeofEventProperty = {
	Connect:(TypeofEventProperty,(any)->nil)->RBXScriptConnection,
	ConnectParallel:(TypeofEventProperty,(any)->nil)->RBXScriptConnection,
	Wait:(TypeofEventProperty)->any
}
local _:TypeofEventProperty = Instance.new"BindableEvent".Event

This generates no warning, which indicates that BindableEvent.Event is considered to be a table type by the type checker.

3 Likes

It appears this happens for all events, e.g. .Changed.
Using the same set up with .Changed instead of .Event shows that the .Changed event’s :Wait() and :Connect()s are expecting to return/pass a single string value.

I think the reason for this is that the type RBXScriptSignal acts as a base type and the actual automatically generated type isn’t identical because the return results/arguments are automatically generated, and those are incompatible with the RBXScriptSignal base type.

I don’t think that the lack of warning is indicating that the type is considered a table I believe that’s just how the type checking works.

Afaik, this doesn’t define a table type it just defines a type that has these specific properties. If you want it to check against that you actually have to specify that the below is a table with the table type.
Edit: I don’t actually think there is a table type or a way to differentiate between userdatas and tables

type TypeofEventProperty = {
	Connect:(TypeofEventProperty,(any)->nil)->RBXScriptConnection,
	ConnectParallel:(TypeofEventProperty,(any)->nil)->RBXScriptConnection,
	Wait:(TypeofEventProperty)->any
}

In conclusion I don’t think this is as much of a bug as much as it is a weird quirk with how the types are generated but that could probably use an addressal. The type of .Changed is more specific than RBXScriptSignal therefore the two types do not match.

1 Like

Typed Luau rejects converting a non table type to a table type, in all circumstances.

--!strict
local x:{Value:any} = Instance.new"NumberValue" -- W000: (2,1) Type 'NumberValue' could not be converted into '{| Value: any |}'

Certainly NumberValue contains a Value property, but it can’t be converted to a table type. If something can be converted to a table, then it must be a table type.

What I mean by table type is specifying the type to be a table, and the values contained in the table, if any. A type considered to not be a table by the type checker can’t be converted to a table, so that is a way to see if a type is considered a table.

1 Like

I’m probably wrong or misunderstanding but based on the way I understand luau I believe this is because you’re trying to convert it to a type that only has a Value property. I am thinking you aren’t referring to the type itself being actually considered a table but are referring to the way you define the type so I think I am just not getting what you’re saying.

For example, these display no warnings despite newproxy returning a userdata because generic userdatas are equivalent to the {} type:

--!strict
local a:{[any]: any} = newproxy()
local b:{} = newproxy()

The type of Instance.Changed is not RBXScriptSignal its not a base type therefore its compatible with a generic type but if it were RBXScriptSignal you wouldn’t be able to do that.

I think the reason is because in most cases the type is defined as something else and you can’t go from different base types, for example, if you try to do this with a Vector2 it doesn’t work because of Vector2 being its own base type and {} being in a sense its own base type too, they are seen as completely different things even though Vector2 is describing

1 Like

This fails too:

--!strict
local x:NumberValue&{Value:any} = Instance.new"NumberValue" -- W000: (2,1) Type 'NumberValue' could not be converted into '{| Value: any |}'

The type for newproxy incorrectly assumes that it returns an empty table. The type checker doesn’t even have knowledge of a userdata type.

--!strict
local x:number = newproxy() -- W000: (2,1) Type '{|  |}' could not be converted into 'number'

Generic userdatas aren’t equivalent to a {} type, consider passing true to create a metatable for the userdata.

--!strict
local x = newproxy(true)
local y:{} = getmetatable(x) -- W000: (3,27) Type '{|  |}' could not be converted into '{ @metatable a; {-  -} }'

(any they also get a different result from type and typeof)

Enums have a similar problem of this not working as expected

--!strict
local x:Enum = Enum.NormalId -- W000: (2,1) Table type 't1 where t1 = {| Back: Enum.NormalId, Bottom: Enum.NormalId, ... 5 more ... |}' not compatible with type 'Enum', has extra fields 'Back', 'Bottom', 'Front', 'GetEnumItems', 'Left', 'Right', and 'Top'

Enum.NormalId is an Enum, but it can’t be converted to the generic type.

{[any]:any} can be used as the generic table type, if something can be converted to it then 'tis a table type.

--!strict
local h:{[any]:any}
h = {} -- ok
h = {1,2,3} -- ok
h = Vector3.new() -- nope
h = Enum -- ok
h = Enum.NormalId -- ok
h = Enum.NormalId.Back -- nope
h = Instance.new"Part" -- nope
h = Instance.new"BindableEvent".Event -- ok
h = Instance.new"RemoteEvent".OnClientEvent -- ok
h = 1 -- nope
h = "a" -- nope
h = RaycastParams.new() -- nope
h = require -- nope
h = script -- nope
1 Like

Thanks for the report! We’ve filed a ticket to our internal database and we’ll follow up when we have an update for you.

4 Likes

This is still a significant issue. If I specify that something is a generic RBXScriptSignal I expect it to accept any kind of signal but it just doesn’t accept any signal at all. Here’s some incredibly basic code that should work but doesn’t.

local function a(x: RBXScriptSignal)
    return x
end

a(Instance.new("Part").Changed) --> Type 'RBXScriptSignal' could not be converted into 'RBXScriptSignal'

An annoying workaround is to define the type of RBXScriptSignal as something like typeof(Instance.new("BindableEvent").Event) but this is tedious and we shouldn’t need a workaround for something this simple. Even the built in autocompletion gets the type correct.
25RfucT

I haven’t come across a single signal that you can use when it expects an RBXScriptSignal.

4 Likes

The core of the issue is that the type of .Changed is more complex than just the signal because it has a Connect method which has a signature that’s different for different types, and the way we’ve implemented signals in the type system doesn’t work with that.

We’re working on fixing this.

11 Likes

Is there any way to extract the parameter types from an RBXScriptSignal? i.e. if I make a function like connect<T...>(RBXScriptSignal, (...: T...) -> ()) to work around some crazy engine bug, how do I infer T from the signal that was passed in?

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.