In strict mode, you can’t add fields to an ascribed table without it flaring.
local t = {} :: {a: number}
t.b = 2 -- bad!
Meaning Car.__inde
x, Car.new
and every method will raise an error.
In strict mode, you can’t add fields to an ascribed table without it flaring.
local t = {} :: {a: number}
t.b = 2 -- bad!
Meaning Car.__inde
x, Car.new
and every method will raise an error.
Any workaround to do private fields and private methods?
None currently, besides just denoting them with _
or overriding the table type with a fake interface.
Maybe with readonly properties, but that’s a long way off.
Doing underscore still makes the fields and methods visible.
--!strict
export type Animal = {
Name: string,
Greet: (self: Animal) -> nil,
} & any
local Animal = {}
Animal.__index = Animal
function Animal.new(name: string): Animal
local self = setmetatable({}, Animal) :: Animal
self.Name = name
self._age = 2 -- Hidden
return self
end
function Animal._jump(self: Animal) -- Also hidden
-- Jump here
end
function Animal.Greet(self: Animal)
print(self.Name, "says hi!")
end
---------------------------------------------------------------------------------
export type Dog = Animal & {
Greet: (self: Dog) -> nil,
}
local Dog = setmetatable({}, Animal)
Dog.__index = Dog
function Dog.new(name: string): Dog
local self = setmetatable(Animal.new(name) :: any, Dog) :: Dog
self._age += 2 -- Should be 4, right?
return self
end
function Dog.Greet(self: Dog)
-- Overriding the Greet method from Animal class
-- The Greet method from Animal could still be accessed by doing
-- Animal.Greet(self)
self:_jump() -- Will not be shown but exists
print(self.Name .. ", jumping, says hi!")
end
Here, by doing export type Animal = {} & any
you allow the table to have private fields without getting an error or exposing them on the type itself.
This is the best workaround I could find so far. If you have suggestions, please, let me know!
Best regards,
Octonions
Very nice guide, thank you very much
If anyone wants a terser automatically typed way to write classes, I’ll leave this here:
--!strict
local Something = {}
Something.__index = Something
function Something.new(isSomething: boolean): Something
local self = setmetatable({}, Something)
self.isSomething = isSomething
return self
end
export type Something = typeof(Something.new(...))
-- methods go here (they don't get typed correctly above the type export)
function Something.becomeNothing(self: Something): ()
self.isSomething = false -- type is inferred from the new function
self.isSomething = 1 -- TypeError: Type 'number' could not be converted into 'boolean'
end
return Something
Class method parameters also get correctly typed. I’m not sure why having the functions after the type export works but before doesn’t, it does work though.
What I mean by “methods don’t get correctly typed above the type export” is that methods above the type export don’t even end up existing in the type, even though they get auto-completed. I’ve only tested and seen this with Luau LSP, not the Roblox Studio script editor, though that’s likely an issue there too.
local Foo = {}
Foo.__index = Foo
function Foo.new(): Foo
local self = setmetatable({}, Foo)
self.fuelTank = FuelTank.new(100)
self.engine = Engine.new()
return self
end
function Foo.start(self: Foo): ()
self.engine:start()
end
export type Foo = typeof(Foo.new(...))
local fooObject = Foo.new()
fooObject:start() -- TypeError: Type 'Foo' does not have key 'start'
return Foo
This approach is suited for composition or just not using any form of class co-dependence at all, see the below example.
I always use composition over inheritance for multiple reasons, but the fact that we can get automatically generated types like this is a really convincing bonus (for me anyway!):
local FuelTank = {}
FuelTank.__index = FuelTank
function FuelTank.new(litres: number): FuelTank
local self = setmetatable({}, FuelTank)
self.litres = litres
return self
end
export type FuelTank = typeof(FuelTank.new(...))
local Engine = {}
Engine.__index = Engine
function Engine.new(): Engine
local self = setmetatable({}, Engine)
self.started = false
return self
end
function Engine.start(self: Engine): ()
self.started = true
end
export type Engine = typeof(Engine.new(...))
local Car = {}
Car.__index = Car
function Car.new(): Car
local self = setmetatable({}, Car)
self.fuelTank = FuelTank.new(100)
self.engine = Engine.new()
return self
end
export type Car = typeof(Car.new(...))
function Car.start(self: Car): ()
self.engine:start()
end
return Car
It just works.
"Class": {
"prefix": "class",
"body": [
"local ${1:Foo} = {}",
"${1:Foo}.__index = ${1:Foo}",
"",
"function ${1:Foo}.new($2): ${1:Foo}",
" local self = setmetatable({}, ${1:Foo})",
"",
" $3",
"",
" return self"
"end",
"",
"export type ${1:Foo} = typeof(${1:Foo}.new(...))",
"",
"${0:-- functions go here (they don't get typed correctly above the type export)}",
"",
"return ${1:Foo}"
]
},
"Class Method": {
"prefix": "method",
"body": [
"function ${1:ClassName}.${2:doSomething}(self: ${1:ClassName}$3): ${4:()}",
" $0",
"end"
]
}
(Put in your user snippets for lua)
When using this method to typecheck classes, it works perfectly inside of the class which the type is being created for. Once I use the exported type, I get some weird behavior.
More specifically, when I first set the type to a local variable, I get complete auto-filling for said class:
Once you call a method from the class, you no longer get auto-filling for your variable with the class its been set to, as seen below:
I’m not sure if this tutorial was made for exporting the type outside of the module which the class is being created inside of, but I tried it anyways and ran into this behavior. If anyone can help me prevent this from happening, I’d greatly appreciate it.
Even if this cant be fixed, thanks for the tutorial and this is still a very useful tool!
Nice! I do prefer composition so I might refer to this later when I decide to switch to type checking OOP
I’m having a hard time reproducing this. Is KillerObject
a type defined in the file or is it supposed to be exported from someone else? Normally, exported types are given a namespace:
local Module = require(script.Module)
local Value: Module.Type = Module.new()
I noticed that in nocheck
mode a bugged ghost type of the module’s name exists which can be confusing. I recommend trying strict
mode to see if that’s what’s going on and changing it to KillerObject.KillerObject
.
If I understand the question correctly, yes. KillerObject
is a type which is exported from a separate module and is imported through the way you specified. Just to be sure I’m not doing something wrong, heres a dumbed down class, but its the exact same way I wrote KillerObject
.
Dumbed Down Class
local class = {} :: Type
class.__index = class
type self = {
StringValue: string;
BoolValue: boolean;
}
export type Type = typeof(setmetatable({} :: self, class))
function class.new() : Type
local self = setmetatable({} :: self, class)
self.StringValue = 'String'
self.BoolValue = false
return self
end
function class:Method()
print('I\'m doing something')
end
edit*
I just tested something right now, and found out the reason why it’s doing this, at least on my end. So if you refer to my original screenshot, I set the method killerInteraction:GetBlockCount()
to a variable. When its set to a variable, it disabled auto-filling for the rest of the function.
local function something()
local variable = class:GetSomething() -- Would be auto-filled
class:GetSomethingToo() -- Would NOT be auto-filled
end
class:GetSomething() -- Would be auto-filled
class:GetSomethingToo() -- Would be auto-filled
You can have private fields by just doing
function Animal.new(name: string): Animal
local self = setmetatable({}, Animal) :: Animal
self.Name = name
local age = 2 -- Hidden
return self
end
You can’t access those local variables in the object’s methods.
You can make public functions from inside the object, which access the private variables.
Not efficient. Instead of holding a method for many different objects you will be filling up the memory with things that could have been better described.
An alternative method I saw someone else suggest on the forum somewhere:
local Something = {}
Something.__index = Something
local privateVariables = {}
function Something.new(name: string): Something
local self = setmetatable({}, Something) :: Something
self.nothing = nil
privateVariables[self] = {
name = name,
} -- tadaaaa
return self
end
function Something.doAThing(self: Something): ()
print("Doing a thing with", privateVariables[self].name)
end
Credit doesn’t go to me.
Where’s the type definition? Or am I just not understanding this right?
I omitted it for simplicity of the example but it’d look something like this if I wrote them:
type PrivateVariables = {
name: string -- yippee! type safety
}
loca privateVariables: { [Something]: PrivateVariables } = {}
local Something = {}
Something.__index = Something
function Something.new(): Something
local self = setmetatable({}, Something)
self.nothing = nil
privateVariables[self] = {
name = name,
} -- tadaaaa
return self
end
type Something = typeof(Something.new(…)) -- right under new, before methods
function Something.doSomething(self: Something): ()
privateVariables[self].name = 2 -- type warning! i think… i’m on mobile so i can’t test it
end
return Something
nice tutorial. Is there a way to access created types from modules? I use module.Type and it sort of works but I can access things like .__index
Adding export
before a type makes it accessible under the namespace the module gets required by.
-- Module.luau
export type MyType = number
return nil
-- Server.luau
local MyModule = require(Module.luau)
local A: MyModule.MyType = 1
Unfortunately having .__index
exposed is a side effect of capturing the whole class table as a metatable.
However, you can set Car.__index
to just a new table and then put the methods inside it. It wastes more memory, but it fixes your problem.
local Car = {}
Car.__index = {}
-- Same Stuff...
function Car.__index.Boost(self: Car): ()
self.Speed += 50
end
guess I gotta cope lol. anyway thanks