Type checking possible nil (number?) in nested metatables seems impossible?

--!strict
local constructorClass = {}

local Constructee = {}
Constructee.__index = Constructee

export type constructee = typeof(setmetatable({}, Constructee)) & {
	test: number?
}

function constructorClass.new(): constructee
	local self = {}
	
	self.test = nil --> could be nil or a number
	
	setmetatable(self :: any, Constructee)
	return self :: constructee --> Cannot cast '{ test = nil }' into Constructee & blah blah because the types are unrelated.
end

return constructorClass

There isn’t a warning when I remove --!strict but I want to know what’s causing the warning in the first place?

For context, the self.test number variable may or may not exist in my experience due to in-experience factors, so I made it number? which should’ve allowed this type cast?

How can I properly type check while allowing for nil in this scenario?

1 Like

Even exporting the type as test: number | nil still brings a warning and also when I use number? and self.test = 100. I’m unsure how to get what I want in this situation.

Edit: After declaring it as test: number & nil, the warnings have been silenced, but I don’t know if this is a proper solution because this is for custom types and that’s not what my intentions are.

Edit 2: I also tried setting self.test as self.test = 521 :: number? and it silenced the warnings again but this is noisy and I’m looking for a better solution because if I do add custom types then asserting possibly nil values will be really ugly.

1 Like

Maybe something like this?:

--!strict
local constructorClass = {}
constructorClass.__index = constructorClass

--Types
export type constructorClass = {
	test: boolean,
	saveKey: string,
}

function constructorClass.new(exampleString: string): constructorClass
	local self = {}
	setmetatable(self, constructorClass)

	self.test = false
	self.exampleString = exampleString

	return self
end

return constructorClass
1 Like

The problem here is that you making a type with metatables, where in this situation it isn’t needed.

--!strict
local constructorClass = {}

local Constructee = {}
Constructee.__index = Constructee

export type constructee = {test: number?}

function constructorClass.new(): constructee
	local self = {}
	
	self.test = nil --> could be nil or a number
	
	setmetatable(self :: any, Constructee)
	return self
end

return constructorClass
1 Like

Can you explain why it isn’t needed? Since the constructor class is returning a metatable, I thought it’d be necessary.

What if I had subconstructor objects, like so:

function constructorClass.new(): constructee
	local self = {}
	
	self.test = subConstructorClass.new()
	
	setmetatable(self :: any, Constructee)
	return self
end

How would I typecheck that?

No, you would have to use typeof to get the actual type like this:

export type constructee = typeof(constructorClass.new())

Alright.

I tried your solution and it works fine, but it doesn’t get methods cause presumably it wasn’t defined as a metatable?

I see why. It doesn’t pick up methods because there’s no index to them. Yeah, sorry but i’m not sure. You would have to create the methods inside the function which probably isn’t desired.

That isn’t desired, but I think I found a solution.

--!strict
local constructorClass = {}

local Constructee = {}
Constructee.__index = Constructee

export type constructee = typeof(setmetatable({}, Constructee)) & {
	test: number?
}

function constructorClass.new(): constructee
	local self = {}

	self.test = nil

	return setmetatable(self :: any, Constructee) :: constructee -->magik solution
end

return constructorClass

No warnings. Does this look appropriate to you? I’m not entirely sure what my change did so I’d like to hear your opinion on what I even did differently.

1 Like

Yeah, this should work fine. Just remember that type errors don’t come up during gameplay, so if it works then it works.

1 Like

This might not be correct. You used the word “any” but it’s important to know the specific types of all values all the time, especially if you’re using --strict mode. this could be

--!strict
local constructorClass = {}

local Constructee = {}
Constructee.__index = Constructee

function constructorClass.new(): constructee
    local self = {}
    self.test = nil
    setmetatable(self, Constructee)
    return self 
end

export type constructee = typeof(constructorClass.new())

return constructorClass
1 Like
--!strict
local constructorClass = {}

local Constructee = {}
Constructee.__index = Constructee

function Constructee:kill()
	print"gg"
end

function constructorClass.new(): constructee
	local self = {}
	self.test = nil
	setmetatable(self :: any, Constructee)
	return self
end

export type constructee = typeof(constructorClass.new())

constructorClass.new():kill() --> kill doesn't exist

return constructorClass

I can’t seem to use methods with this code. It might be a simple fix but at this point it feels like I’m wandering in the dark with this.

in my case it does work

Untitled

Looks like a Roblox bug?

I forgot to delete any in my first post

1 Like

Thanks for the help. My final issue is if I wanted to use inheritance, like so:

--base class
local constructorClass = {}

local Constructee = {}
Constructee.__index = Constructee

function constructorClass.new(): constructee
	local self = {}

	self.BaseClassNumber = 24

	setmetatable(self, Constructee)
	return self
end

export type constructee = typeof(constructorClass.new())

return constructorClass
local baseClass = require(script.Parent)

--subclass (inherits from code above)

function constructorClass.new(): constructee
	local self = baseClass.new()
	
	self.test = "testing"
	
	setmetatable(self, Constructee)
	return self
end

How could I prevent warnings like these?

Untitled

I would also like to know the same thing.
The problem is that the base class is sealed and you can’t add any more members.
Type checking and OOP are a headache. That’s why I try to use OOP as little as possible.

1 Like

I feel like a solution for my inheritance idea is to use my former semi-solution where I’m a tad unsure of it’s members and their types while using your solution for the absolute sub classes.

In my project, I’m not inheriting from objects but inheriting functions and general data, so the reasons you listed why my original solution wasn’t a good one may not apply to my situation.

I’ll take some aspirin and code my thinking into reality and I’ll reply if it worked or not. Thank you for the help though. OOP and type checking are headaches.

1 Like

After digging into OOP type checking topics and blindly guessing in the dark on how to fix my code, I believe I’ve achieved heavenly status of code that works in a favorable way for my experience.

OOP Headache.rbxm (1.9 KB)

Warning: Nightmare-inducing OOP structures

Before looking into the code, I tried my best to follow the Luau styling guide as if everyone in the world is going to judge this script with that level of intensity. Please correct any errors if you see them.

--!strict

local furnitureClass = {}

local Furniture = {}
Furniture.__index = Furniture

export type class = typeof(setmetatable({}, Furniture)) & {
	seatOccupied: boolean,
		
	seat: () -> any?
}

function Furniture:sit(): ()
	self.seatOccupied = true
end

function furnitureClass.new(): class
	local self = {}
	
	self.seatOccupied = false
	
	return setmetatable(self :: any, Furniture) :: class
end

return furnitureClass

This is the furniture base class. It holds data and methods relative to all furniture objects. For my example I’ll only have a singular couch subclass but it can be expanded for arm chairs, stools, etc.

I created the constructor function outside of the furniture class to prevent useless code smell where you could, without error, do Furniture.new().new().new().new(). When using auto complete to navigate my methods and values, I find that I would always see .new() even when it was an already constructed class object, making it 100% useless.

--!strict

local Furniture = require(script.Parent)

local couchClass = {}

local Couch = {}
Couch.__index = Couch

export type class = typeof(Furniture.new()) & typeof(Couch) & {
	pulledOut: boolean,
	sleeping: boolean,
	
	sleep: () -> any?,
	pullout: () -> any?
}

function Couch:sleep(): ()
	if self.pulledOut then
		self.sleeping = true
	end
end

function Couch:pullout(): ()
	if not self.seatOccupied then
		self.pulledOut = true
	end
end

function couchClass.new(): class
	local self: Furniture.class & any = Furniture.new()
	
	self.pulledOut = false
	self.sleeping = false
	
	return setmetatable(self :: any, Couch) :: class
end

return couchClass

There’s not much to say. The couch class inherits values and methods from the furniture class while also constructing some of its own. If you wanted your couch to have a plushie or anything you could also use composition and it’d work well if it was predefined in the export type declaration.

Also, again, I separated the couch constructor and class to prevent .new() clog when using the Studio auto complete feature.

--!strict

local Couch = require(script.Parent:WaitForChild("Furniture"):WaitForChild("Couch"))

local myCouch = Couch.new()
myCouch:pullout()

And finally, the code that made it all worth it. Not really, but.

Untitled

Untitled

As you can see, when using the variable myCouch, the auto complete will allow me to see everything. It also doesn’t have the .new() option because it’s already created.

Anyways, I dislike this level of OOP and type checking, I don’t even know if what I’ve done here is correct. I think this was too complicated for something as simple as a couch and it’s borderline evil.

@lumizk & @su0002, enjoy.


Edit 2: ~10 minutes after I posted this, I got a notification relating to this code posted here:

I think their approach easily supersedes mine and I suggest checking it out if inheritance type checking / autocomplete support is something you need.

1 Like

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