Request for clarification of luau type checking documentation

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.

8 Likes

Hey! I agree that this section needs updating. When this document was written, the new version that replaces it was simply not possible at all at the time.

I’ve made a pull request over at https://github.com/Roblox/luau/pull/578 that gives this page a facelift. Feel free to submit feedback on that. Another thing we’ll do is add these examples into our tests, so they don’t break or serve as a reminder to update documentation.

4 Likes