Luau Type Checking: Support adding missing fields outside of table declaration

Often we have cases in our code where we want to create some sort of table of a specific type, and then after declaring it with its static values, we want to also add some extra fields that need to be done outside of the declaration (often because the the extra field being declared also needs to reference the table itself).

This is not allowed by the type checker, however, as it only does the type checking within the initial declaration of the table - even when there isn’t any code using the table between the first declaration and the appending of the missing field.

For example:

type example = {
	staticData: boolean;
	dynamicData: RBXScriptConnection;
}
local x: example = {
	staticData = true;
}
x.dynamicData = script.AncestryChanged:Connect(function()
	x.staticData = false
end)

This type checking error can be avoided by making an empty declaration of the table, then saving all of the dynamic values to variables, then building the entire table in one go:

local x: example;
local dynamicData = script.AncestryChanged:Connect(function()
	x.staticData = false
end)
x = {
   staticData = true;
   dynamicData = dynamicData;
}

But this is kind of annoying and backwards - it’s not in a very logical order. It would be preferable to have this code structured in the first way.

5 Likes

While I do recognise that it’s an example and there’s probably a production-level use case out there that you ran into that made you create this request, sample one can be fixed by using the shorthand for union typing nil, type?. That way, dynamicData can exist as either nil or RBXScriptConnection.

type example = {
	staticData: boolean;
	dynamicData: RBXScriptConnection?;
}

Not sure if you were trying to avoid that though, nor if you should.

Relevant read: sealed tables.

2 Likes

Yeah, you can solve the error it that way, but you are also right that I am trying to avoid that method. Having the type checking becomes less useful here since we are now allowed to forget to set the missing field everywhere else.

2 Likes

So define them all in the table constructor? (create the connection there as well?)

local x: example
x = {
	staticData = true;
	dynamicData = script.AncestryChanged:Connect(function()
		x.staticData = false
	end)
}

Because of sealed tables, your feature request won’t work, and would only result in confusing code. I also believe this is more what you want than pre-making the connection then setting the dynamicData field to that.

2 Likes

I suppose. It’ll get kinda messy if you’ve got a lot of code to define that way though.

I disagree, I think it would be fine as long as we had a good error messages for it. For example, the following case where I would expect there to be an error:

type example = {
   staticData: boolean;
   dynamicData1: RBXScriptConnection;
   dynamicData2: RBXScriptConnection;
}
local x: example = {staticData = true}
x.dynamicData1 = signal1:Connect()
-- the use of x before being completely defined causes the error
-- resolved by moving dynamicData2's definition to before the function call
func(x)
x.dynamicData2 = signal2:Connect()

So that’s kind of what I’m thinking. The type checking shouldn’t be based just on the table itself, but also when it gets used.

Also note the main problem I have with the type checker at the moment is the amount of friction it adds to development. This seems to be another one of those things that adds friction without much good reason from what I can tell. I don’t like having my time wasted trying to wrangle my code to follow the type checker’s pretty strict requirements, and it’s why I don’t use the type checker when writing normal code (so far at least, maybe I’ll grow more comfortable with it once I know how to handle all of the special cases without wasting lots of time and effort).