How do I add type checking to this class module? (OOP)

Hello there.

I am following this object oriented programming tutorial:

The problem is, there’s no intellisense right now, when I try to use a class I created using Rcade.

So how should I go about modifying Rcade to add type checking so that I’ll have intellisense when instantiating an object and calling its methods?

What I have surmised so far

I know that the construction method is just the call meta method in Rcade. So if I want intellisense for that, I will probably have to create a separate meta table for each class built using Rcade, find a way for the class to give the type parameters for the init method to Rcade, and then insert a unique __call method into each class’ metatables that have those specific types so that I’ll have intellisense when calling it.

Unfortunately, I have not ever touched generics (or even a lot of type checking) in my life, so I have no idea how to actually pull that off.

By the way, I use the new (beta) type solver, so feel free to assume that in your proposed solution.

Hey there, first I feel like I should apologize because Rcade was written before I knew how to work with types.

Here are the steps I would follow to include intellisense to Rcade:

  1. Go into your class ModuleScript(s) and at the top, replace the local ExampleClassName = Rcade.class{} with just local ExampleClassName = {}
  2. One line below that, add a type definition that accepts the type of the class table and export it, like so: export type ClassType = typeof(ExampleClassName) This will dynamically create the type of the class table.
  3. At the bottom of the class module scripts, replace return ExampleClassName with return Rcade.class(ExampleClassName)
  4. Currently the dynamicly created type will only include the class methods, because the properties are set within the constructor method to the object table, rather than the class table. So to include object properties, above the __init() constructor method, set a nil value for the property the object should contain, example: Server.ExampleProperty = nil :: string (this declares that the server table has a property named ‘ExampleProperty’ of type string)
  5. For every method in the class, update the method declarations from : to . and add the self parameter to each function, and delcare it of type ClassType (Or the name of the class type) Example:

Old method format:

function Server:onJoin(player: Player)
	print(player.Name .. " joined")
end

New method format:

function Server.onJoin(self: ClassType, player: Player)
	print(player.Name .. " joined")
end
  1. Finally, when you create an object instance of a class, you declare it to be the type that’s exported from the class ModuleScript. Example:
local ExampleClassName = require(game.ServerStorage.ExampleClassName)
local newExampleClassName: ExampleClassName.ClassType = ExampleClassName()

Now there should be intellisense when you try to index a property or method!

Here’s the Server class example after following the above steps:

local Rcade = require(game.ServerStorage.Rcade) 
local Server = {}
export type ClassType = typeof(Server)

Server.ExampleProperty = nil :: string

function Server.__init(self: ClassType)
	self.ExampleProperty = "Example Value"

	Rcade.Utility.eventListener(self, game.Players.PlayerAdded, "onJoin")
	Rcade.Utility.eventListener(self, game.Players.PlayerRemoving, "onLeave")
	Rcade.Utility.bindToClose(self, "onClose")
end

function Server.__tostring(self: Class)
	return "Server"
end

function Server.onJoin(self: ClassType, player: Player)
	print(player.Name .. " joined")
end

function Server.onLeave(self: ClassType, player: Player)
	print(player.Name .. " left")
end

function Server.onClose(self: ClassType)
	print("The server is closing")
end

return Rcade.class(Server)

And here’s creating a Server object and declaring the exported type:

local Server = require(game.ServerStorage.Server)
local newServer: Server.ClassType = Server()

Hope this helps! Let me know if something isn’t working or needs more explanation, I wrote this on the fly. I’m sure there are other ways to implement this, but I feel like this is the simplest way without re-writing how Rcade works at its core.

1 Like

You can use the type keyword to define a type.

type Math = {
    x: number,
    y: number
    add: () -> number
}

function use(math: Math): number
   return math.add()
end

This defines a type that represents the shape of your class, so you can’t put function definitions INSIDE the type definition. It might not be viable for you, but the article you mentioned didn’t show this keyword, so it’s worth mentioning.

1 Like

Hi, thanks a lot for taking the time to answer my question. While this approach works, it is quite cumbersome to have to set each field to nil in the class and then typecast it. Is there not a simpler way to tell the type solver the shape of the object? I was thinking of, something like this:

just do :: setmetatable<{},module>

The examples you showed work different in how the constructor behaves, since in Rcade the constructor does not itself create the object, it only modifies the object that’s passed to it. So, properties can’t be dynamically created into a type using the constructor method as is. You would have to modify Rcade to create the object and return it within the constructor, and you would have to change how Rcade works from the inside.

One of my focuses on Rcade was to make the constructor as simple as possible. And call me crazy (I know I am) but I still like defining class properties (including object properties) outside of the constructor.

Good luck on your OOP endeavors!

1 Like

Can you elaborate? I am not sure what you mean…

Where exactly do I add this?

just create a type with that
replace {} with your table properties (the one that is not a metatable)
and assign type to it.
You also completelly not need __call becouse:

  1. Its stuipid and reinventing the wheel all over again
  2. you can just make a constructor outside of metatable like:
local module = {}
module.__index=module

type GenericType = {Name:string;DisplayName:string}
type ClassType = setmetatable<GenericType,typeof(module)>
local Generic:GenericType = {Name="";DisplayName="";}

local function Constructor(Name:string,DisplayName:string):ClassType
local self:GenericType = table.clone(Generic)
self.Name=Name
self.DisplayName=DisplayName

return setmetatable(self,module)::ClassType
end

return Constructor

For more information: Type checking - Luau

As you know, __call was just a way to get rid of the constructor from the class table. Directly returning the constructor also doesn’t make sense if you want to use class properties / methods without needing an instance.

My current implementations avoid using __call now, opting to go with a local function, but I don’t think __call is a bad solution either. I used to like creating an instance of a class with python syntax: Class()

There are definitely some cool things with the new type solver, I think the conclusion is that for what OP wants, they’ll either have to re-write Rcade, or try a different implementation. However, my previous example would probably as close as you can get while keeping Rcade intact, which the post title suggests.

1 Like

Ye just like a great way to deoptimize the code by like 25%

Metatables in general (exept for __mode one) should be avoided unless you are very tight on memory (metatable OOP for example)
But yet again 1M tables is like 16MB of RAM only and i think perfomance > memory consumption that otherwise would not get noticied anyway unlike perfomance.
If you add __call metamethod instead of making independant function each time you call table Luau will have to check for existance of metamethod first and only then call it which is awful to all sorts of micro optimizations.
More so its fully irational to do that as since the code i provided above literally mimics __call but instead you are using dirrect function call.

1 Like

Deoptimizes the code sure, but almost no one will hit the limits with __call unless you’re creating instances like crazy, and if you are ECS is probably a better system for that in general. But you’re right that a direct local function call for the constructor is more efficient and is what I currently implement as well.

And metatables are great for implementing method overriding, while still being able to access the overridden methods. I love my metatables!

Sometimes in the pursuit of optimization, we can make programming harder for ourselves, and the project never even gets done. I’m trying to find a happy middle path. Rcade was one of my first attempts at that, over three years ago.

This discussion is becoming off topic from the main post, so I’ll move off from this thread now. Happy coding everyone!

Never once i have seen optimizing the code delay that.

Metatables is fully avoidable and i would even argue that use of them makes code harder to manage.
The last time i used metatables was for automatic gc key collector inside a table (__mode=“kv”)
Everything else inside it can be mimicked and would look even better sometimes.
Even metatable OOP is already surpassed with strict typechecking OOP and generic table “clonning” and not to mention true OG “closure OOP”.
Yeah sure you have to store referance to a function but that literally cost nothing and is more begginer friendly actually instead of metatable OOP.

Could you please elaborate on what does

means?
It’s very important to not go into personal conflicts or attack someone and keep the thread on topic, as otherwise it would violate the TOS of DevForum.

1 Like