How do I type check object methods?

Hello. I am facing an issue with type checking object methods. This is what I am currently doing,

destroy: (_object) -> () -- _object is referencing self

but the autofill forces me to use a dot in an inherited class.
image
image

How would I get the type checker to make me use a colon?

1 Like

Use self as the name of the argument ie destroy: (self: _object) -> ()

Thanks for the reply! However, still prompts me to use dots.
image

Can you show how you’re assigning the _object type to self, also can you show the entire type definition for _object

type _object = {
	class_name: string,
	id: string,
	_data: {
		created: number,
		instances: {},
		connections: {},
	},
	
	destroy: (self: _object) -> ()
}

function object.new(class_name: string): _object -- where _object is used
	local self = setmetatable({}, object)
	self.class_name = class_name
	self.id = http_service:GenerateGUID(false)
	
	self._data = {
		created = os.time(),
		instances = {},
		connections = {},
	}
	
	return self
end

I see, my guess is that it’s expecting you to write your destroy function inside the object.new function. Can you try something like:

type object_props = {
    class_name: string,
    id: string,
    _data: { 		
        created: number,
		instances: {},
		connections: {},
    }
}

type object_methods = {
    destroy: (self: object) -> ()
}

type object = typeof(setmetatable({} :: object_props, {} :: { __index: object_methods }))

and see if assigning object to self (ie local self: object = setmetatable...) gives you your intended result?

That’s a lot of syntax I don’t really know yet. Just wondering, what do the double colons do?

Sorry about that, it’s a type cast which is used on expressions to refine types when the inferred type is too broad.

So in your case, with a table, when you’re doing type t = typeof(setmetatable({}, {})), with no type casting, you’re really saying that both the table and the metatable can both have any type of entry with any type of key and any type of value, with no set interface.

However when you type cast as in my post above, you’re saying that the table will contain the entries in object_props, and the metatable will have a __index metamethod which points to a table that will have the members of object_methods.

Thank you! However, this code sample is in an abstract class, and the metatable has to be saved for future inheritance and some composition elements.

Ah okay that’s no problem, you should be able to export the type (export type object = ...) then in your modules that will inherit your class, you can just use an intersection type to inherit from the child class.

Ex:

object module:

export type object = typeof(setmetatable({} :: object_props, {} :: { __index: object_methods }))

now say we want our class cool_object to inherit object, we’d just do

local object = require(path.to.object)

type cool_object_props = {
    cool: boolean;
}

type cool_object_methods = {
    doSomething: (self: cool_object) -> ()
}

-- here we get the intersection type of cool_object and object using &

type cool_object = typeof(setmetatable({} :: cool_object_props, {} :: { __index: cool_object_methods })) & object.object

I don’t know; you think we could backtrack to something a bit more simple?

I can try, was there a certain part that was confusing?

1 Like

Kind of the whole syntax part. I don’t know too much about this, but I’m not sure if that much is needed to just change an autofill from recognizing a function or method.

Sorry if I sound a little rude, it’s a little late for me lol

You aren’t being rude, no worries.

I believe you do have to do this much though because you’re going to run into an unrelated type error when you try to set the type of a table with metatable to be the type of a normal table without a metatable

Another option would be to switch to nocheck mode by doing --!nocheck at the top of your script but then you don’t get any of the other warnings that would point out possible issues in your code

Okay, thank you.

Also just wondering, another subclass is returning in this other modulescript. You can see that the autofill works.
image
image

Now for some reason, when I require that module from a separate script, the autofill doesn’t work. Would you know why?
image

Can you show the relevant parts of your scripts (namely when and what you’re returning, when you’re requiring the module, and when you’re creating and indexing the object)?

All of this occurs during initialization.

This is what is being returned.

--object--
local object = {}
object.__index = object

--composition--
local entity = require(replicated_storage.framework_shared.classes.abstract.entity)

--types--
type _object = typeof(setmetatable(entity.new(script.Name), object)) & {
	func: () -> () -- an attribute which holds a function that is passed through with .new
}


--class init function--
function object.new(input_bind_list: {computer: Enum.KeyCode?, console: Enum.KeyCode?}, output_bind_list: {computer: Enum.KeyCode?, console: Enum.KeyCode?}, func: () -> ())
	local self: _object = setmetatable(entity.new(script.Name), object)
	self.func = func
	
	--hidden code
	
	return self
end

--class methods--
function object:fire()
	self.func()
end

return object

The other end is a local script insignificant code occurring above the screenshot I sent.

Oh I must have misunderstood, I was thinking object was going to be the base class that other classes inherit, not the other way around.

I believe the issue stems from the fact that calling setmetatable on a table that already has a metatable won’t update the table’s metatable in the type checker.

So to get around this you’d have to update your types in the entity module first:

local entity = {}
entity.__index = entity

-- important parts below:

type entity_props = { -- this has all the properties in `entity`
	entityProp: boolean
}

type entity_methods = { -- this has all the `entity` class methods
	entity_method: (self: entity_type) -> ();
	--__index: entity_methods; -- optionally __index can also go here
}

-- below is where we set the __index metamethod for the type checker
-- export type will export this type so they're accessible elsewhere
export type entity_type = typeof(setmetatable({} :: entity_props, {} :: { __index: entity_methods }))

function entity.new(name: string): entity_type
	local self = setmetatable({}, entity)

	return self
end

function entity:entity_method()

end

return entity

Then in your object module:

--composition--
local entity = require(script.Parent.entity)

--object--
local object = setmetatable({}, entity) -- in this case, setmetatable allows `object` to inherit the `entity` class methods
object.__index = object

--types--
-- properties that `object` will have
type object_props = {
	func: () -> ()
}

-- again, class methods
type object_methods = {
	fire: (self: object_type) -> ();
}

-- finally set the object to be a type:
export type object_type = typeof(setmetatable({} :: object_props, {} :: { __index: object_methods })) & entity.entity_type

--class init function--
function object.new(input_bind_list: {computer: Enum.KeyCode?, console: Enum.KeyCode?}, output_bind_list: {computer: Enum.KeyCode?, console: Enum.KeyCode?}, func: () -> ()): object_type
	local self: object_type = setmetatable(entity.new(script.Name), object)
	self.func = func

	--hidden code

	return self
end

--class methods--
function object:fire()
	self.func()
end

return object

Finally, as a test:

local object = require(script.Parent.object)

local obj = object.new()
obj:fire() -- ok; auto complete shows up
obj:entity_method() -- ok; auto complete shows up

obj.func() -- ok; auto complete shows up
local p = obj.entityProp -- ok; auto complete shows up
3 Likes

Thank you very much! It’s real late for me, so I’ll try this tomorrow morning.

1 Like

Also, my OOP system is a little weird. I have one base abstract class. All it has is an “entity id” and a few other things such as the destroy method. All classes inherit from “entity”, but from there, it works with composition, and the classes can use each other as components.

1 Like