Classes for luau module

Also, I apologize, I am not a super high frequency on the development forums and I did not recognize your status here. And my tism goes off when people say things that don’t match documentation to persuade someone to do something that perhaps don’t need to do.

So my apologize on the doc hard siting you there. My bad.

1 Like

Just locally or using a lot of exporting? And have you figured out a more reliable format of returning from a module in a way that infers the typing so that I don’t have to hard reReference every time.

CraneStyle is my dicord, hit me up, would love to compare. I have done a little type script work as I edge into it, but some of my experiments have been quite frustrating.

The one you linked is even more recent than mine is! I’ve forgotten just how long ago I’ve made my module. I do quite like how it includes public and private fields, but I don’t like how they’ve been implemented. It feels very clunkly to define those fields and I wish they were written better. This does give me some ideas for if I ever revive my class system… Maybe some day?

This implementation made by @ChatGGPT use fenv to create non-local variables which should be avoided in practice and reconsidering what I mentioned earlier about how types could be added, any benefit they could provide wouldn’t work here as fenv breaks type-safety.

Maybe I am dumb but I still don’t get what you guys mean by type annotation for classes

How would this look like???

Any environment modification disables optimizations. So yeah that’s true.

How so? You should make a feature request or an issue on the guy’s GitHub page if you have any feedback for it.

class.rbxm (1.7 KB)

Ok guys new small update now it lets you do

class {
    ...
}

Maybe what they’re talking about is like when you do

class "Person" {
	constructor = function()
		this.name = "Unknown"
		this.age = 0
		print("Person created: " .. this.name .. ", Age: " .. this.age)
	end,
	greet = function()
		print("Hello, my name is " .. this.name)
	end
}

local john = new(Person)()
john.name = "John"
john.age = 30
john:greet() <- there’s an autocomplete when you’re trying to write greet

I dont think you are able to make dynamic type annotations, if im wrong please correct me

There shouldn’t be, I tried many solutions but none worked so far for my own class module. You have to do it manually unfortunately.

Yeah, I’ve tried making it before too and haven’t found any solutions, I really hope roblox adds a way for dynamic type annotations

1 Like

What do you mean by ‘dynamic?’ Is it something like generics?

-- example
local function create<T>(name: string, props: T): { content: T }
    return { content = props }
end

local t = create("string", {hello=1})
print(t.content.h --> 'hello' autocompletes here

For a class constructor, you could possibly define the class as something like this:
(Class Type)

export type LUA_CLASS<Props, Name> = {
    className: Name, -- doesn't really need to be here, but it's just here for the example
    constructor: nil --> there's no real way to remove it from type analysis, so just set it to nil
} &Props

(Constructor function)

-- Create class constructor function
local function class<P, N>(name: N)
    return function(props: P): LUA_CLASS<P, N>
        --- ... return class data
    end
end
Or if you want to get real fancy with it
local function class<P, N>(name: string &N)
    return function(props: {[string]: any} &P): LUA_CLASS<P, N>
        --- ... return class data
    end
end

This will scream at the developer with type errors if they do not implement the correct type of argument.

N can be anything (but it must be a string); P can be anything (but it must be a string-indiced table) [STRICT MODE ONLY]


(Autocomplete)

-- Usage in scripts
local myClass = class "myClass" {
    hello = "world"
}

print(myClass.cl --> this will autocomplete the 'className' property from the type
print(myClass.he --> this will also autocomplete the 'hello' property that was defined in the constructor

I’m not sure If this is what you were referring to though (cc @TenebrisNoctua)

2 Likes

wait…why this kinda

hear me out. i just fw the interface. i love you for the module

I didnt know you could use generics like that, thanks for the info (yes i was referring to that)

On a normal metatable wrapper to create a class module, yours would work perfectly fine, you actually wouldn’t even need a solution like this since typeof(setmetatable(...)) also works.

The problem is, I have an access specifier system with multiple tables in a classData table type, and it needs to be dynamic.

local TestClass = class "Test" {
  Public = {
    hi = "hello"
  }
}

local newTestObj = TestClass.new() -- Should be: {hi: string}

How would I transfer the type information from the Public and other access specifiers into the the final newTestObj object? Do you have a solution for that?

How am I supposed to do that bro

For your specific example, I was able to create this:

--!strict
export type LUA_CLASS<Public = {[any]: any}?> = {
	new: (...any) -> Public,
}

local function class(name: string)
	return function(props)
		return {} :: LUA_CLASS<typeof(if props.Public then props.Public else nil)>
	end
end

local myClass = class "myClass" {
	Public = { roblox = "2018" }
}

myClass.new().ro --> 'roblox' autocompletes here

However, this makes Public become mandatory as opposed to optional. The main issue comes from the fact that when using generics, the type analysis doesn’t believe that P can contain an index for “Public” (which makes sense because generics can be anything) even if you pass it:

local function class(name: string)
	return function<P>(props: P)
		return {} :: LUA_CLASS<typeof(if props.Public then props.Public else nil)> 
        --                               ^ TypeError: Type 'P' does not have key 'Public'
	end
end

local myClass = class "myClass" {
    Public = { roblox = "2018" }
}

And you can’t use an intersection because you’ll lose the autocomplete. This is where I would say some sort of Luau type conditional would be useful (I basically tried to do that with the typeof statement).


I haven’t tried the new type solver just yet, so there may or may not be some differences there.

1 Like

I’ll do some experiments and reach out again, if that’s alright. Didn’t know you could use generics like this.

That’s what I actually needed for this, it’s very dynamic and a Luau type conditional keyword would be required for me to do this.

Hey how could I implement this for mine

I don’t know how to add typechecking for metatables

I re-typed part of your module to this:

function extends(class)
	return get_class(class) :: typeof(if type(class) == "string" then nil else class)
end

function class(a : ClassIdentifier): <T>(props: T) -> T
	local checkconstructor = function(k, v, c)
		if k == "constructor" then
			assert(type(v) == "function", "class field uses reserved keyword 'constructor'")
			assert(c.constructor == nil, "classes may only have a singular constructor")
		end
	end

	local new_class = a and setv({}, a) or {}

	local result = {
		__call = function(_, ...)			
			assert(#{...} == 1, "unexpected syntax")

			local body = ({...})[1]

			assert(type(body) == "table", `unexpected '{type(body)}'`)

			for k, v in pairs(body) do
				checkconstructor(k, v, new_class)

				new_class[k] = v
			end

			return new_class
		end,
		
		__index = function(t, extended_class)
			return function(...)
				local new_class = t:__call(...)

				for k, v in pairs(extended_class) do
					new_class[k] = new_class[k] or v
				end

				if new_class.constructor then
					setv(function(...)
						assert(extended_class.constructor, "unexpected 'super'")

						return extended_class.constructor(...)
					end, "super", new_class.constructor)
				end
			end
		end
	}

	return type(a) == "table" and result:__call(a) or setmetatable({}, result)
end

function new(class)
	local v = class
	local class = get_class(class)

	return function(...)
		local instance = copy_t(class)

		for k, v in pairs(instance) do
			if type(v) == "function" then
				setv(instance, "this", v)

				if k == "constructor" then
					v(...)

					instance[k] = nil
				end
			end
		end

		return instance :: typeof(if type(v) == "string" then nil else v)
	end
end

return 
	function(): (typeof(new), typeof(class), typeof(extends))
		return new, class, extends 
	end

In order to make it work, like for the other issue I ran into, most of the parameter types had to be removed in order for the conditionals to work correctly (which isn’t ideal).

This doesn’t work for classes with a string reference since those can’t be properly inferred by the type analysis, so this only works if you directly pass the class’ object to the functions

1 Like

Woah I didn’t know you could do something like that