I want to use OOP programming in Roblox while also benefitting from Luau. I haven’t read up on the specific benefits of conforming to strict Luau type checking so I don’t know how necessary it is, but I’m sure it will feel good to use the “proper” programming style. I’m having some problems doing this, though, and I was hoping that the documentation could be clarified so things are a bit more clear on what to do. Also, are there any performance benefits using strict type checking?
According to the documentation, one line is all that’s required to conform idiomatic OOP to Luau type checking:
Typing idiomatic OOP
One common pattern we see throughout Roblox is this OOP idiom. A downside with this pattern is that it does not automatically create a type binding for an instance of that class, so one has to write type Account = typeof(Account.new("", 0))
.
local Account = {}
Account.__index = Account
function Account.new(name, balance)
local self = {}
self.name = name
self.balance = balance
return setmetatable(self, Account)
end
function Account:deposit(credit)
self.balance += credit
end
function Account:withdraw(debit)
self.balance -= debit
end
local account: Account = Account.new("Alexander", 500)
--^^^^^^^ not ok, 'Account' does not exist
After trying this myself using strict type checking and adding the line of code, I found that object methods do not populate the self
type. This is not much of a problem because the syntax can be rearranged to the pythonic style:
function Account.deposit(self: Account, credit: number)
self.balance += credit
end
With this modification to the declarations, syntax highlighting has 0 warnings.
The big problem I am having is calling object methods in the constructor. Consider the following three constructors:
function Account.new1(name: string, balance: number): Account
local self = {
name = name,
balance = 0,
}
setmetatable(self, Account)
Account.deposit(self, balance)
return self
end
function Account.new2(name: string, balance: number): Account
local self = {
name = name,
balance = 0,
}
self = setmetatable(self, Account)::Account
self:deposit(balance)
return self
end
function Account.new3(name: string, balance: number): Account
local self = {
name = name,
balance = 0,
}
self = setmetatable(self, Account)::Account
Account.deposit(self, balance)
return self
end
For constructor 1, syntax highlighting flags Account.deposit(self, balance)
as “Type ‘Account’ does not have key ‘deposit.’” This warning can be fixed by moving the function declarations above the constructor function, but another warning appears: see constructor 3.
Constructor 2 is the same as constructor 1 except the type is Type ‘{ @metatable Account, { balance: number, name: string } }’. This warning can be fixed by moving the function declarations above the constructor function, but another warning appears: see constructor 3.
Constructor 3 flags the line self.balance += credit
with the warning “number\nUnknown type used in - operation, consider adding a type annotation to ‘self’”
The one thing I found that worked was to turn the constructor into a wrapper function which actually does the constructing then performs any object methods on it:
type ProtoAccount = {
name: string,
balance: number
}
local function NewAccount(name: string)
local self = {
name = name
}::ProtoAccount
return setmetatable(self, Account)
end
function Account.new(name: string, balance: number)
local self = NewAccount(name)
self:deposit(balance)
return p
end
type Account = typeof(Account.new("",0))
This code feels more bulky than it should be, and I would like to be able to call object methods inside the constructor without syntax highlighting throwing errors.
Another thing is typing across module boundaries. It would be nice if all the types I’ve defined in a module were inherited by scripts that require it.
EDIT:
I’ve refined the above slightly to this:
local Account = {}
Account.__index = Account
type ProtoAccount = {
name: string,
balance: number
}
local function NewAccount()
return setmetatable({}::ProtoAccount,Account)
end
export type Account = typeof(NewAccount())
function Account.new(name: string, balance: number)
local self = NewAccount()
self.Name = name
self:Deposit(balance)
end
function Account.deposit(self: Account, credit)
self.balance += credit
end
Any tables have to be instantiated in the NewAccount function or in Account.new before they can be used. The export
keyword allows requiring scripts to use the Account type.