How do something similar to `class [name] implements [interface]` in Luau (TS)

I’m trying to do something similar to what this video proposes but Luau instead of TypeSctipt.

I want to be able to define a sorta abstract class that defines what inheriting classes should implement. as seen in the video:

an interface is just like a typing a table in Luau
image

I think you should be able to modify metatables and set some custom priorities to recreate this behaviour.

You need a main class which all subclasses will inherit from. Take this example:

local Main = {}

function Main.new()
    local self = setmetatable({}, {__index = Main})
    return self
end

function Main:foo()
    print("From main class: ", self)
end

function Main:foobar()
    print("Called from main class")
end

return Main

Then, let’s take a subclass:

local Subclass = {}

function Subclass.new()
    local self = Main.new() --first, get a main object which we can then modify
    
    getmetatable(self).__index = function(_, k: any): any
        return Subclass[k] or Main[k]
    end
end

function Subclass:bar()
    print("Called from subclass")
end

function Subclass:foo()
    print("Called from subclass")
end

return Subclass

This implementation also allows you to overwrite methods in the main class for the subclass object only.

local MainObj = Main.new()
local SubObj = Subclass.new()

MainObj:foo() --> "from main class:  table: 0x000000"
SubObj:foo() --> "Called from subclass"
SubObj:foobar() --> "Called from main class"

By updating our __index function, we can first search the subclass object for the requested item and if it’s not found there, default to the main object.

If you want to do type annotations for this, you can use the intersect type operator, which is an ampersand (&).

export type MainClass = typeof(Main)
export type Subclass = typeof(Subclass) & MainClass

Not a very helpful answer but I hope you were able to find some alternatives or ideas.

The thing is, I don’t want traditional inheritance because I cant really implement an generic class thing (or whatever).

Like I said, I just want to define should be in a Storage and not it’s implementation. (For type checking reasons…)

Then you can just not make Main a class.
Remove the .new function for Main and modify the subclass’s .new a bit.

function Subclass.new()
    local self = setmetatable({}, {__index = function(_, k)
        return Subclass[k] or Main[k]
    end})

    return self
end

You’ve got to have the functions that will be used somewhere, and here they’ll just be stored in the Main table. We need to use __index to effectively get them from the main class table while keeping the code neat.

Would it be possible to condense this into a simple type table? Like:

export type Main = {
    new: () -> Main,
    foo: (self: Main) -> (),
    bar: (self: Main, arg1: number) -> boolean
    ...
}

You can use a table type for the main class with no issue at all.

Yes, for the subclasses, if you write in the methods for the abstract main class as well. Just remember it will technically be incorrect, due to the fact the methods are accessed from a different table through a metatable, so strict mode will scream at you lol

You could use these for accuracy, but may not provide autocomplete:

type Subclass = typeof(Subclass)
type Subclass = typeof(setmetatable({}, subclass))

--this is technically wrong
type Subclass = typeof(Subclass) & typeof(Main)

Is this what you were asking for? Sorry if not, I’m having trouble understanding what you’re asking.

Thanks for your input! However there may be a slight misunderstanding about what I’m asking for. To clarify:

I don’t want to have to write out all the Main classes functions individually as shown in your original post…

I just want to condense it by just only typing something like the following, without all the extra bloat.


Also by using Subclass[k] or Main[k], wouldn’t that mean that it would fallback to the Main function? (witch again, shouldn’t exist. It should only be a reference for what should be in the Subclass for the Type checker)


I’m using this forum post for my type checking: Guide to Type-Checking with OOP



PS: I’ll be gone for a few hours after writing this, so expect a delayed response.

If I understand you correctly, this is more of a typechecking problem than an OOP problem, and you want an idiom for interface types for classes. Your interface type definition should be a table type but also include a generic for the implementing class so that the self parameter gets correctly typed:

export type Interface<T> = {
	Foo: (self: T) -> (),
	Bar: (self: T) -> number,
}

Then, classes that implement the interface would be typed as a union of the interface and their unique fields:

export type Class = Interface<Class> & {
	Foobar: number,
}

export type SecondClass = Interface<SecondClass> & {
	Doobar: string,
}

So Class will have the method Foo with a self parameter typed as Class (i.e. (self: Class) -> (), and SecondClass will also have the method Foo with a self parameter typed as SecondClass (i.e. (self: SecondClass) -> ()).

You can write functions that take objects deriving from the interface and pass in any implementing class without problem.

local firstObject: Class = Class.new()
local secondObject: SecondClass = SecondClass.new()

local function callFoo<T>(object: Interface<T>)
	object:Foo()
end

callFoo(firstObject) -- Fine
callFoo(secondObject) -- Fine
1 Like

Yes, you got it exactly right! You response is essentially perfect. My only question is:

Where should the Intersection be to emulate what is seen in the video?

should it be here:

type self = Interface<Class> & {
    ...
}

or here:

export type Class = Interface<Class> & typeof(setmetatable({} :: self, Class))

Thanks for your response!


EDIT:

Also do you have to use a Template/Generic Interface<T> type? or can you just do:

export type Interface = {
	Foo: (self: Interface) -> (),
	Bar: (self: Interface) -> number,
}

To not have to know the exact type when running callFoo(), just that is has those values.

Could you be more specific?

I wouldn’t use metatables and typeof to declare types, as the current typechecker produces less-than-ideal types if they have a metatable attached (will be improved soon, though). You only really need to use that syntax if you plan on using getmetatable on your objects, which is mostly unnecessary. Also, I’m not sure how it’ll fare with the way I decided to define the interface.

I’d use the former to declare the type (see my previous example). You also don’t have to use an intersection, if you feel it makes the generated type easier to read. The following retains the same behavior, so you can pass a Class object into anything that wants an Interface and typecast Class objects to Interface objects.

export type Interface<T> = {
	Foo: (self: T) -> (),
	Bar: (self: T) -> number,
}

export type Class = {
	Foo: (self: Class) -> (),
	Bar: (self: Class) -> number,
    Value: number
}

If you don’t have the generic, creating a class object will cause an error (I’m using the V2 type solver, so I don’t know if it’s fine with the old one).

type Interface = {
	Foo: (self: Interface) -> (),
	Bar: (self: Interface) -> number,
}

type Class = Interface & {
	Value: number,
}

local function foo(self: Class): ()
	return
end

local function bar(self: Class): number
	return self.Value
end

-- Not ok because foo and bar expect Interface, 
-- but Class is not exactly Interface
local object: Class = {
	Foo = foo,
	Bar = bar,
	Value = 100,
}

Typing a parameter as an interface means only the defined properties of the interface will be available, which is desired behavior.

For example:

local function doSomething<T>(object: Interface<T>)
    -- Not ok because we only know object has foo and bar methods
    -- TypeError: Key 'Value' not found in table 'Interface<T>'
    -- You can typecast object to Class, and access Value that way, though
	print(object.Value) 
end

local object: Class = {
	Foo = foo,
	Bar = bar,
	Value = 100,
}

doSomething(object) -- This would technically be fine at runtime
1 Like

I’m currently following this Guide for my OOP… Guide to Type-Checking with OOP… so yes. I am using getmetatable.

Thats fine, i can deal with that ig…



I responded tomorrow, happy new year!

Using metatables in your type definition will cause it the interface typecheck to fail. But that’s ok, because you don’t need to use them to declare class types anyways.

local Metatable = {}
Metatable.__index = Metatable

type Class = typeof(setmetatable({} :: {
	Field: number,
}, Metatable))

function Metatable.Method(self: Class): number
	return self.Field
end

type Interface<T> = {
	Method: (T) -> number,
}

local function test<T>(object: Interface<T>) end

local x: Class = setmetatable({Field = 10}, Metatable)

-- Class could not be converted to Interface<T>
test(x)

The above Class can be equivalently typed (without the metatable) as the following. Objects still technically have the metatable attached, but the typechecker doesn’t need to know that.

local Metatable = {}
Metatable.__index = Metatable

type Class = {
	Field: number,
	Method: (self: Class) -> number,
}

function Metatable.Method(self: Class): number
	return self.Field
end

local function new(): Class
	local self = {
		Field = 10,
	}
	setmetatable(self, Metatable)

    -- You will need to typecast here
	return self :: Class
end

type Interface<T> = {
	Method: (T) -> number,
}

local function test<T>(object: Interface<T>) end

local object = new()
test(object) -- Ok
object:Method() -- Ok

-- Not ok, but you'll probably never need to do this
local mt = getmetatable(object)
1 Like

Couldn’t I just do this to save on unnessary redefinetions?

type Interface<T> = {
	Method: (T) -> number,
}

type Class = Interface<Class> & {
	Field: number,
	_PrivateMethod: (self: Class) -> boolean
}


or even just (but probobly not) this:

type self = {
	Field: number,
}

type Class = Interface<Class> & typeof(setmetatable({} :: self, Class))
(altho its giving me a Type Error with that statment)
function Class.new(): Class
	local self: Class = setmetatable({} :: self, Class)
	...
end
TypeError: Type 'self' could not be converted into 't1 where t1 = ...
caused by:
  Not all intersection parts are compatible.
Type 'self' could not be converted into 'Interface<Interface<*CYCLE*>  & { @metatable Class, self }>'

Yeah, that should work if you don’t want to rewrite some parts of the type. Stay away from metatables in type definitions until they’re better supported.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.