Is it worth using "--!strict"?

Is it really worth using “–!strict”?

I am a solo developer making a little indie game. I came across type-checking and how I can use the “–!strict” mode a little bit ago. I talked around and it seemed like using it was the optimal choice for cleaner and more robust coding structures. Once I defined the –!strict mode at the top of my script, everything seemed fine. My scripts felt more controlled and professional. This was working great until I ran into a few problems. Type-check errors—are somehow worse than learning metamethods for the first time. The thing is, I never fully dived into learning type-checking. Yes, I spent time learning what I thought was most of it. But some type-errors I just couldn’t seem to fix. I knew that the –!strict mode would help with pointing out mistakes made by having the wrong type, but it felt like it went way deeper than that. When I first started to realize this wasn’t just some little mode you put on and then enjoy your day, was when it errored for not checking if a variable was nil or not. I had it set to variable: Instance. It wanted me to basically do: if variable == nil then return end. So I did it and went on with my day. BUT THEN, it got a little crazy. This is where I take you to the problem I’m facing right now.

Main Issue:

I’m scripting a procedural generation module at the moment. And I have a little table that deals with the paths at which the generation will take place. Since its a dictionary I use the metamethod “__len” to get the length of it when I need it. This is really annoying though since with arrays you can just add a hashtag to the front of the word without metamethods but whatever Roblox.
The problem is that, the “paths” table gets a type error on our last for loop (pictures of proof is provided).
Snippet of code:

--!strict
local ShipGen = {}
ShipGen.MetaTable = {
	__len = function(tbl) -- gets length of table (usually a dictionary)
		local num for i in tbl do num = i end return num
	end,
}

local startRoom = GetStartRoom()
if startRoom == nil then return end -- check

local paths = {} -- the paths which will generate rooms
setmetatable(paths, ShipGen.MetaTable)

for i, exit in startRoom:GetChildren() do -- place the first exits into the path table thingy
		if CC:HasTag(exit, "Exit") and exit:IsA("BasePart") then
			paths[i] = exit -- I know I can just use "table.insert" but that wouldnt be a dictionary
		end
	end

print(#paths) -- > 2 (The __len metamethod works!)

for number, exit in paths do -- goes through all the possible paths (Type-checker underlines "paths" and errors it for some reason??)
			
		end
return ShipGen

[REMINDER: This is not the entire module script, I’m just showing the code that is important and trying to save time with this]

And our friend “type-checker” doesn’t seem to like this last for loop I placed so it underlines the variable “paths”. It gives the following error:
image
image
Now to be completely honest. I have NO idea how to deal with this type error and what to do so I just kinda came to the dev forums. I most likely didn’t read far enough into how type-checking works and this is the result. I’m PRAYING someone can help me with this because if not I will never figure this out! If anyone needs clarifications on any part of this post I’d be happy to do so!

TL;DR:

If you’re too lazy to read allat, I get it. Hopefully this won’t be hard to understand if you’re a seasoned developer. But the problem i’m having is shown in the above image ^^^. I’m trying to fix a type error with an 80% confused and 20% sleep deprived brain. Reading that error is like trying to read another language. It’s erroring from a “paths” table (dictionary to be exact) I have which is using the __len metamethod. I don’t know how this could be erroring the paths table when im trying to use an iterate function but its happening somehow :sob:.

Side Note:
I wrote this post at 2 am so if im a little unclear PLEASE ask me anything! Also feel free to correct some of my code if there are mistakes or better ways to do things. Fixing this type-error problem is high on my list.

Thanks Devs,
MonsterTrident

6 Likes

It appears to be the metatable that is throwing the type checker off. In strict mode it is assuming that the metatable must be providing an __iter function to use (could be as simple as providing it __iter = pairs, and that works).

2 Likes

I tried this:

__iter = function(tbl)
		return next, tbl
	end,

and then ran my for loop and it seemed to work fine now! I wasn’t aware that when they added for loops without using next or pairs/ipairs they also added that metamethod which can customize the loops (__iter).
Another way of doing this also resulted in the type error going away. This was just wrapping the table in “pairs” but im trying to avoid that because im not sure it will be deprecated in the near future. Same with using “next”, they both worked due to the fact that they can iterate over dictionaries:
image
image
But I have one more question. Should this way of using for loops (without next or pairs) be used only for when using metamethods or should you entirely switch to this way of looping through things? Somewhere on the dev forum I saw a post which stated that pairs and ipairs were going to be deprecated soon. And the post said to use just for _, in table do without pairs or next because it simply switches to the correct way without people getting confused as of what to use.

As the iterator functions aren’t deprecated yet, it still seems the safe way is to maybe just not use them? Or is it still ok to use stuff like this? And is using next ok to use too?

1 Like

I just turn it on occasionally to get a good view of any obvious issues. Most of the pragmatic code that’ll work fine and is readable will be flagged inevitably as wrongly typed somehow when in reality its just being a stickler. There’s also just bugs in the typechecker in general, like it is just outright wrong a lot of the time, especially the new one.

As for iterators (pairs, ipairs), do not use them. The VM now decides what the best is to use based on the tables contents, built into the syntax. Be aware theres annoying things though with __iter also causing __call to run with the built-in iteration.

3 Likes

Ah I see, thanks for clearing most of that up! Also thanks to @TheGrimDeathZombie for helping out with the initial bug too.
To clarify:

  • It’s not usually worth it to use --!strict
  • Don’t use (pairs, ipairs) anymore
  • Be careful when using __iter
  • The ideal way to iterate is: for i, v in table do

Things I still wonder about:

  • Is next still safe to use or is it the same case with the other iterator functions (not to use them)?
  • Are you potentially saying that my type-error I got is something that should be disregarded (You stated there are bugs in the type-checker)?
  • Are you saying to fix my bug with the type-checker, would be to just turn off strict mode altogether? This would allow me to still use the for loop without any pairs or next iterators. And without the __iter function.
  • next() is the internal iterator often used; regardless, you rarely need to ever use it yourself unless you’re making a custom iterator, which is sometimes useful like when handling Page objects. pairs() and ipairs() were made by luaheads forever ago trying to make for loops more readable and they use next() internally.

  • There are bugs in the type-checker, yes. There’s two versions you can use, one in beta and one in release, and both tend to be misleading. Type-checking in Luau is just a formality to make code more readable to other developers; it doesn’t do anything during runtime. So you should focus on that: readability. The current type-checker will spawn issues from thin air often just from if statements, requiring weird fixes like using assert() when you don’t want to. It also doesn’t understand metatables and subsequently metamethods very well. I noticed in your example in particular, it lacks common sense as we have to realize you don’t need __iter to iterate a metatable.

Here’s proof:

--!strict
local ShipGen = {}
ShipGen.MetaTable = {
	__len = function(tbl) -- gets length of table (usually a dictionary)
		local num for i in tbl do num = i end return num
	end,
}

local paths = {} :: {number} -- the paths which will generate rooms
setmetatable(paths, ShipGen.MetaTable)

for i = 1, 10, 2 do
	paths[i] = i
end

print(#paths) -- > 2 (The __len metamethod works!)

for number, exit in paths do -- goes through all the possible paths (Type-checker underlines "paths" and errors it for some reason??)
	print(number, exit)
end

Oooo is it gonna break? No, of course not. Most metamethods are replacements to vanilla behavior, not required to function. The only real exceptions are __call and the mathematical methods (you cant add tables to numbers but with these metamethods you can).

  • I mean yeah, I’m speaking to you in that kind of colloquial staff who’s been working here for 12 years (which is actually accurate in my case). The type-checking really is just a thing they added to allure web developers from JavaScript to Roblox development to grow the developer base. It’s just something to assist you; it’s rarely correct about serious issues because it cannot predict runtime values outside of the immediate structure.

Regardless of mode (strict, nonstrict, etc), you should make your types explicit. Here’s some documentation.

local paths = {} :: {BasePart}

or

local paths : {BasePart} = {}

The difference between the two is the first one is an assertion (I guess its called that? Theres little documentation on this specific method to type). You can type modules this way where a variable doesn’t exist, like x["Example"] = nil :: BasePart? so now when you write x, it’ll auto-complete x.Example and anticipate a BasePart. Makes it much easier to develop with.

Of course that won’t fix your strict issue but it does help with what type-checking is made for: readability and development ease.

Bonus about the dictionary vs. Array I noticed

Also, the dictionary versus array isn’t really useful for this case. Arrays are faster in small batches while dictionaries are faster in larger batches due to hash solving. I wouldn’t swiss-cheese an array to make a dictionary; I’d just use string indices. The reason why is because during runtime you may hit edge-cases where it is actually an array due to coincidence (like 1,2,3,4 and nothing after being a value) but you don’t realize it, causing you to destroy everything when you do table.remove() or x[1] = nil later. You can also hit problems with DataStores because they cannot accept swiss cheese dictionaries, it’ll jumble everything in the JSON string. I broke a character save slot system doing that once.

Also, notice how your __len method is inaccurate in my example. It’ll return the last key, not the actual length. You’d need to loop it to get the true length of a dictionary. The # operator works only with arrays given it performs regression. (It overshoots by *2 and then divides down until it hits a non-nil and zeroes in, which is faster than iteration for large arrays, and doesn’t work at all for dictionaries).

1 Like

Thanks so much! I’ll use type-checking in module scripts for readability but won’t use !strict anymore. Also thanks for pointing out than dumb mistake I made with the __len metamethod. If you don’t mind me asking, does type-checking hurt your performance? Especially if you’re using it a lot, would it impact your game? If the answer is yes, then I’ll be using it very carefully for maximum optimization.

No not at all, it doesn’t actually run during runtime, its just for the IDE to help you code.

1 Like

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