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.
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.
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)
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?
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.
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