Differentiating type class methods from "." indexed functions

Roblox classes such as Instance have methods that can only be indexed via :.

For example the when indexing a Part Instance with : autocomplete will autofill with methods.
These methods do not appear when indexing the same Part with .
For example doing Part. shows results for things like Anchored, etc. Properties.
However you cant index methods like Destroy through . autofill will only show these through :

How do you reflect this behavior in an OOP type class?

type myClassImpl = {
	__index: myClassImpl ;
	new: (initNumber: number) -> myClass;				-- to be indexed with .
	-- methods : funcName: (self: myClass) -> returnVal
	GetNum: (self: myClass) -> number;					-- to be indexed with :
};

type Proto_myClass = {
	NumberVal: number;
};

local myClass: myClassImpl = {} :: myClassImpl ;
myClass.__index = myClass;

export type myClass= typeof(setmetatable({} :: Proto_myClass, {} :: myClassImpl));

function myClass.new(initNumber): myClass
	local self = setmetatable({} :: Proto_myClass, myClass);
	self.NumberVal = initNumber;
	return self;
end;

function myClass:GetNum(): number
	return self.NumberVal;
end;

myClass. --> autofill shows the GetNum method... I only want this method to show when indexing with :

Obviously its something wrong with the way ive setup the type class and the casting of types but even moving the methods to a separate methods type and whatever, i cant figure out how to get it to work.

in the return of the .new or the self declaration is where i would have to insert the methods type table containing type definitions for the functions i want to be methods but idk, cant wrap my tiny head around it LOL. help pls…

3 Likes

Using a ’ : ’ is just a short way of passing self as the first argument, I don’t think that there is a way to hide the suggestions based on if you’re using a ’ . ’ or a ’ : '.

Note: You can access methods with a period by just manually passing in the instance like this:

workspace.Baseplate.Destroy(workspace.Baseplate)

Not sure how they’re hiding it (source code perhaps?), but I’ll try looking into it.

1 Like

I understand the differences in : and . as I use the both of these in their respective use cases but in going with my own luau grammatical rules I’d like to index any method call with :. Therefor hiding it from auto fill is a desire.

I feel this is achievable by removing the type definition of methods from the myClassImpl type table and adding the methods to a separate type table and in the .new() function set the return type of self to be like, local self = setmetatable({} :: Proto_myClass & myClassMethods, myClass);

But I can’t get this to work, the methods don’t appear afterwards when trying to access them via:

local test = myClass.new(5);

test: -- no autofill and trying to index the method doesn't work.
1 Like

I don’t know what’s causing this to not give you intellisense. I copied your code verbatim and can’t replicate this on my end.
image

The only suggestion I have is maybe try enabling the new Luau type solver beta?
image

1 Like

Could you show the entire source? Theres probably something I’ve messed up on my end lol

Here you go, I also tested strict typing and that didn’t seem to change anything for me. If this doesn’t work I would try enabling the beta, I could also show you a class example that doesn’t use metatables to see if that would work.

type myClassImpl = {
	__index: myClassImpl ;
	new: (initNumber: number) -> myClass;				-- to be indexed with .
	-- methods : funcName: (self: myClass) -> returnVal
	GetNum: (self: myClass) -> number;					-- to be indexed with :
};

type Proto_myClass = {
	NumberVal: number;
};

local myClass: myClassImpl = {} :: myClassImpl ;
myClass.__index = myClass;

export type myClass= typeof(setmetatable({} :: Proto_myClass, {} :: myClassImpl));

function myClass.new(initNumber): myClass
	local self = setmetatable({} :: Proto_myClass, myClass);
	self.NumberVal = initNumber;
	return self;
end;

function myClass:GetNum(): number
	return self.NumberVal;
end;

--myClass: -- will not intellisense here, myClass object will construct w/ methods after .new is called

local sup = myClass.new(1);
sup

Interesting… it doesn’t auto fill but plugging it in anyways and running it in the console gives the expected result…
image

Also I’m unsure if I want to move to the new type checker right now as it’s still in beta and I’ve seen a lot of forum posts reporting bugs surrounding it. I assume its in a late stage of development and could be pushed soon implying a lot of these bugs have been fixed and are old but I would still rather stick to a known stable version even tho it comes with all of its downsides. Unless the new type checker is currently considered fairly stable then I may switch to it now.

Ohh dude I don’t think you get what this post is about.. :sweat_smile:
I’m talking about hiding the methods from myClass. i.e myClass.GetN... won’t show an autofill result for GetNum()

Like how if you try to index Destroy through Part via . It doesn’t show up as an autofill, however it is still a valid way to call Destroy you just have to pass in self as a parameter i.e. Part.Destroy(Part)

The only way that you can do this is to break apart the ‘myClass’ variable (the whole class) so that only the returned value from the constructor (the object) has the methods, instead of the whole class with the constructors and all. Right now, you’re using the whole class as the indexed metatable so the methods are mixed inside of it. I once tried this, but it wasn’t worth changing in my opinion. I can try to write an excerpt on how to do this if you want, or maybe you can try it if you don’t need any further help.

1 Like

Ah I see, in that case I don’t think it would be possible keeping your method contained within that class.

1 Like

I thought as much, but I had issues getting the implementation to work in code, maybe I’m just too sleep deprived :upside_down_face:. Thank you!

1 Like

Here’s an example of how it might look:

--!strict
local ClassConstructors = {} :: ClassConstructors

local ClassMethods = {} :: ClassMethods
ClassMethods.__index = ClassMethods

type ClassConstructors = {
	new: (value: number) -> ClassObject
}

type ClassMethods = {
	__index: ClassMethods,
	Method: (self: ClassObject) -> ()
}

type ClassProperties = {
	Value: number
}

export type ClassObject = typeof(
	setmetatable(
		{} :: ClassProperties,
		{} :: ClassMethods
	)
)

function ClassConstructors.new(value: number): ClassObject
	return setmetatable({
		Value = value
	}, ClassMethods)
end

function ClassMethods:Method()
	print(self.Value)
end

return ClassConstructors

This should fix this problem, if you think it is worthwhile to have it set up this way.

1 Like

Unfortunately this still seems to intellisense method using the dot operator.
image

1 Like

seems so but at that point you might as well remove the constructors type if its only going to store 1 value… :sweat_smile:

Thank you regardless, I’ll take a look in the morning when I have a fresh mind and weigh the pros and cons of how much it bugs me and how needless the set up for this class is.

1 Like

I believe that this is impossible to circumvent. Methods can be called either way, and there’s no way to distinguish between dot and semicolon notation with the standard type solver. Only Roblox has the power to create impossible types for us, like Enums and semicolon-only methods. Maybe the New Type Solver will be different, but last time I tested it was extremely buggy and showed thousands of type errors in my game.

@0x_Integrate seems so but at that point you might as well remove the constructors type if its only going to store 1 value… :sweat_smile:

Yeah, I kept it that way to show that you can add as many constructors as you’d like, to be more flexible, since that’d be the case with a traditional style.

2 Likes

You can sort of do this with typecasting. Here’s how I set up my own classes, basically:

-- Class is its own type
export type Class =
{
    -- Methods
    Method: (self: Class) -> ();
    -- Members
    someValue: number;
}
-- Separate static functions from class methods
type Static =
{
    new: () -> Class;
}
-- Implementation is completely separate
type ClassImpl =
{
    __index: ClassImpl;
} & Static & Class -- Union the static functions and the class methods

local Class: ClassImpl = {} :: ClassImpl
Class.__index = Class

function Class.new(): Class
    local self: Class = {
        someValue = 2;
    } :: Class
    setmetatable(self :: any, Class)
    return self
end

-- VERY IMPORTANT!
-- The typecast makes it so only the static functions are exposed to other scripts
return Class :: Static

So, only your methods and members are exposed for anything that is of type Class. But, your static functions like the constructor are limited to whatever requires the module.

3 Likes

That’s a pretty interesting method. It’s a little bit hacky looking lol. It took me a minute to read and understand. I suppose this is another way for the OP to do this by messing with types instead of tables themselves. Probably saves a bit of memory too. I’m not sure what would happen if you tried doing this with this style of classes:

type Predicate<Value> = (value: Value) -> boolean

type ValueWrapperProperties<Value> = {
	_Destructor: Destructor.Destructor,
	_Destructing: BindableEvent,
	_Changed: BindableEvent,
	Value: Value,
	Changed: RBXScriptSignal,
	Predicates: {[string]: Predicate<Value>},
	Destructing: RBXScriptSignal
}

export type ValueWrapper<Value> = typeof(
	setmetatable(
		{} :: ValueWrapperProperties<Value>,
		ValueWrapper
	)
)

(Edit: I forgot this, but this style of class isn’t supported by IntelliSense at all, actually. I think it is an issue with setmetatable, so scrap that idea, lol)

Anyway, unfortunately, it seems that the OP’s goal was to emulate Roblox classes, so that IntelliSense only displays methods when you use a semicolon, which, to my understanding, is impossible, unless you have another hacky trick up your sleeve ¯_(ツ)_/¯

(Edit 2: Whoops, I’ve accidentally been calling them semicolons instead of colons. I didn’t mean that.)

2 Likes

After you get used to it, it’s actually not hacky. It only looks like that at first glance because it unions the two types into the implementation, but it’s way better than something like export type Class = typeof(setmetatable({} :: { Method: (self: Class) -> () }, {} :: ClassImpl)).

When it comes to this, it’s impossible. They do something internally so methods can’t be exposed by invoking them via . rather than a colon. I likely misread the post, but at least I was able to shine a light on another way of doing class typing.

2 Likes

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