I’ve noticed that when I make a type that uses setmetatable<>, any optional properties become hard to work with.
I would assume that the following structure is valid:
--!strict
type TestObject = setmetatable<{
required: string,
optional: string?
}, {}>
local function new(): TestObject
local object: TestObject = setmetatable({
required = "",
}, {})
return object
end
however the New Type Solver says that { required: string } is not a subtype of { required: string, optional: string? }. Okay, I guess it wants me to make it explicit that I want “nil” there (which is weird since it is not a read-only property) so this should work, right?
--!strict
type TestObject = setmetatable<{
required: string,
optional: string?
}, {}>
local function new(): TestObject
local object: TestObject = setmetatable({
required = "",
optional = nil
}, {})
return object
end
Fooled again! Now it is going to complain that string is not nil (since the value is supposed to be an union of string and nil). If I provide it an empty string, it is going to complain that nil is not string. To go around this, I have to retype this manually:
--!strict
type TestObject = setmetatable<{
required: string,
optional: string?
}, {}>
local function new(): TestObject
local object: TestObject = setmetatable({
required = "",
optional = nil :: string? -- or `:: index<TestObject, "optional">` for more complex structures
}, {})
return object
end
Expected behavior
I would expect not having to explicitly define all optional properties which comes hand in hand with the New Type Solver not recognising either value of “optional unions” when they are inside of a metatable. It works outside of metatable but not inside - is this desired behaviour?
--!strict
type TestObject = {
required: string,
optional: string?
}
local function new(): TestObject
local object: TestObject = {
required = "" -- no metatables, no complains
}
return object
end