Metatable Chaining Incompatible With Type Checking?

I want to create a table with an assigned metatable inherit methods from another metatable. Assume that we’re using the --!strict type solver here.

Let’s say you have a type and table of properties.

type properties = {
	name: string,
	power: number,
	walking: boolean
}

local properties: properties = {
	name = "norman",
	power = 4,
	walking = false
}

And then you have a table of methods and a class type.

local methods = {}
methods.__index = methods

type object_class = typeof(setmetatable({} :: properties, methods))

methods.start_walking = function(self: object_class)
	self.walking = true
	print(self.name.." is walking!")
end

You create a class type object by assigning the methods table to the properties table with setmetatable.

local object: object_class = setmetatable(properties, methods)

Works fine, right?

object:start_walking() -- "norman is walking!"

Now, let’s say that you want to add extra methods and an inherited class type.

local cool_methods = {}
cool_methods.__index = cool_methods
setmetatable(cool_methods, methods) -- build on top of existing methods

type cool_object_class = typeof(setmetatable({} :: properties, cool_methods))

cool_methods.do_a_flip = function(self: cool_object_class)
	self.power += 2
	self.walking = false
	print(self.name.." did a flip, woah!")
end

You create the inherited class type object, but…

local cool_properties: properties = {
	name = "coolio",
	power = 2,
	walking = false
}
local cool_object: cool_object_class = setmetatable(cool_properties, cool_methods)

cool_object:start_walking()
cool_object:do_a_flip()

The methods aren’t available in autocompletion despite being there!!!


image

Full Script
--!strict



--// Base Object

-- properties
type properties = {
	name: string,
	power: number,
	walking: boolean
}

-- methods
local methods = {}
methods.__index = methods

type object_class = typeof(setmetatable({} :: properties, methods))

methods.start_walking = function(self: object_class)
	self.walking = true
	print(self.name.." is walking!")
end

-- creation
local properties: properties = {
	name = "norman",
	power = 4,
	walking = false
}

local object: object_class = setmetatable(properties, methods)
object:start_walking() -- "norman is walking!"



--// Inherited Object

-- methods
local cool_methods = {}
cool_methods.__index = cool_methods
setmetatable(cool_methods, methods) -- build on top of existing methods

type cool_object_class = typeof(setmetatable({} :: properties, cool_methods))

cool_methods.do_a_flip = function(self: cool_object_class)
	self.power += 2
	self.walking = false
	print(self.name.." did a flip, woah!")
end

-- creation
local cool_properties: properties = {
	name = "coolio",
	power = 2,
	walking = false
}

local cool_object: cool_object_class = setmetatable(cool_properties, cool_methods)
cool_object:start_walking()
cool_object:do_a_flip()

That leaves me with a few questions…
Is this intentional? Is there a better method for creating class inheritance in Roblox? Should I even use metatables for OOP in the engine?

Let me know what you think, and thanks for reading :slight_smile:

1 Like

try change type object_class to:

= typeof(setmetatable({} :: properties, {} :: typeof(methods)))

doesn’t seem to work, it still returns the same warning on cool_object:start_walking()

I think it will work

-- abstract class

local Animal = {}
Animal.__index = Animal

function Animal:walk()
  print("animal walks")
end

--

local Cat = setmetatable({}, Animal)
Cat.__index = Cat

type Cat = typeof(setmetatable(
  {} :: {
    name: string
  },
  {} :: typeof(Cat)
))

function Cat.meow(self: Cat)
  print(`[{self.name}]: says meow!`)
end

constructor = function(name: string): Cat
  local self = {name = name}

  return setmetatable(self, Cat)
end

local cat = constructor("mycat")

cat:walk()
cat:meow()

There is a limit to the depth that typesof can infer. It can infer from the table in __index, but not from the table in the __index of the table in __index. What you have to do is declare those members explicitly:

type cool_object_class = typeof(setmetatable({} :: properties, cool_methods)) & object_class

There are no longer any type errors, but for some reason not all methods appear in Studio’s autocompletion.



If you’re using the new type solver, there should be getmetatable and setmetatable type functions. They might work better for you than typeof.

My hot take is that you probably don’t even need to use metatables in this context. Say we want a base type, we can describe it like this:

type Base = {
    doSomethingBase: () -> ()
}

now I could be mistaken but you should be able to make a “constructor” function that takes a table and assigns the appropriate values. This also has the benefit of the table’s length operator (#tbl) returning an accurate result. So we can do something like:

function constructBase(tbl: {[any]: any}): {[any]: any} & Base
    function tbl:doSomethingBase(_)
        print("base method idk")
    end
end

Then in order to “inherit” it, I could use an intersection type and chain constructors to make the type

type CoolType = {
    doCoolThing: () -> ()
} & Base

function CoolType.new(): CoolType
    return constructBase({
        doCoolThing = function() print("this is cool") end
    })
end

Might need some typecasting to get fully working but I believe this should correctly report the types? I’m kinda freestyling this without opening studio but if it doesn’t work I’ll go look at it more in detail