Weird behaviour of an optional property of a metatable

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

Currently strict mode on the new type solver is not very fond of optional fields. You should make an Issue on the GitHub repo.

Hello! Yes, sorry to say that the new solver’s strict mode is still a bit finicky when it comes to optional fields and especially setmetatable. We are hoping that in the near future the pattern:

--!strict
type TestObject = setmetatable<{
	required: string,
	optional: string?
}, {}>

local function new(): TestObject
    return setmetatable({
        required = "",
    }, {})
end

… will work as expected, and there are some bug fixes coming down the pipe otherwise.

1 Like

I’m glad to know it is a bug and not me misunderstanding how types work again. Should I take it as you, Roblox, taking this bug report into account and returning back when it is fixed or should I make a GitHub Issue as @ishtar112 mentioned?

A DevForum bug is something we will take into account and track! You’re all set here.

2 Likes