Hello, fellow human beings! I’m Zamdie, and I’m gonna be your guide today! ヽ(・∀・)ノ
In this tutorial, I will explain Luau type checking in a very beginner-friendly way. Check out the official page if you’re knowledgeable.
0.1: Prerequisites
- How functions work; See this article for information.
- Variables; See this article for information.
- Tables: how indexes/keys and values work; See this article for information
- Events: arguments and their types; See this article for information
- Loops; See this article for information, for table loops see the table article
That’s all!
1: Getting started
First things first, remember to put --!strict
on the top of the scripts you want typechecking.
Now, let’s talk about Vanilla Lua types.
Vanilla Lua has 8 types:
- Numbers:
1
,2
,3
,4
,5
; (only floats, in other words, decimal numbers), type isnumber
- Strings:
"Type checking"
,"is awesome!"
; (text), type isstring
- Booleans:
true
,false
, ornil
, which is the same asfalse
; (truthy or falsy), type isboolean
- Tables:
{"First key", "Second key"}
, there isn’t a table type to type check, but you’ll learn how to create your own - Nil:
nil
; (nothing, or non-existent), type isnil
- Any: represents every single type. (use when you don’t know what type the variable is going to be), type is
any
- Userdata:
userdata
(non-Vanilla Lua types, such as Roblox classes and objects), you can’t type check those - Function:
function
(a function), cannot type check you actually can, but still doesn’t work. Has been announced that you will be able to, hopefully in the near future
You can use all these types except userdata
and function
when type checking!
2: How to add type notations
To add a type notation, it’s really simple. Just add a colon, and the type of the variable/parameter after it.
local variable: typenotation
local typeChecking: number
We’ve just created the variable typeChecking
, with the type number
. Now if we try to assign a different type to it…
We attempted to give typeChecking
the a value of type string
, but since we already made it only accept the type number
, it gives us a warning.
This works exactly the same with all types.
local typeChecking: boolean
typeChecking = 1
Error, because typeChecking
can only be true or false, because we assigned it the type boolean
, but we assigned 1, of type number
!
local typeChecking: string
typeChecking = true
Again! typeChecking
only accepts strings, and we assigned it to true
, which is a boolean
!
What if we wanted to assign the variable in the same line it has been declared? Is there a reason to?
Not really, because of the type inference, however, there are exceptions, where I’d say it would be perfect to (I’ll explain later).
Though, there is no point in doing this:
local numberTypeChecking: number = 1
local stringTypeChecking: string = "Stop right there!!"
local booleanTypeChecking: boolean = true
3: Roblox classes/objects, datatypes and Enums as types
Every single roblox class/object is a valid type for type checking. See this for a list of them. Enums and datatypes are too.
Wait, but how do I note those types when creating a variable??
Same formula as before! local variable: typenotation
Let’s use an Enum as a type first:
local enumTypeCheck: Enum.UserInputType
enumTypeCheck = Enum.UserInputType.MouseButton1 -- Cool! That's a valid type
enumTypeCheck = Enum.OverrideMouseIconBehavior.ForceHide -- Not valid D:
enumTypeCheck
has been noted as a Enum.UserInputType
, so when we attempt to set it to another type of Enum (Enum.OverrideMouseIconBehavior
) it type errors.
Here’s an example using the BrickColor
datatype:
local datatypeTypeCheck: BrickColor
datatypeTypeCheck = BrickColor.Red() -- Valid type
datatypeTypeCheck = Vector3.new() -- Since when is Vector3 a Brickcolor?
We assign it the datatype BrickColor
to it, and when we attempt to set it to a Vector3
, it type errors.
What about Roblox classes/objects?
For example, let’s use the class/object Part
:
local part: Part
part = Instance.new("Part")
What happens if I create a RemoteEvent
instead of a Part
, while the variable has the type Part
noted?
-
RemoteEvent
is the same asPart
, so it’s gud ( ´ ω ` ) - No lol u stooped??!!1 (・`ω´・)
It’s number 2!
What about this example?
local part: BasePart
part = Instance.new("Part")
part = Instance.new("MeshPart")
- What sorcery is this??? Of course it’s wrong! (#`Д´)
- The image from the wiki shows
Part
inherits fromBasePart
, and I also saw by my own thatMeshPart
does too. (︶▽︶)
Number 2 is correct again!
What???
That’s not magic, that’s programming right there my friend!
Just kidding, it’s not that complicated.
Continue reading!
3.1: (Optional) How inheritance and abstraction works
Inheritance is when a sub-class/object inherits their ancestor’s properties and methods. But they can also have their own, and their children can inherit those.
Look at the screenie of the Part
object family. It is under BasePart
, and same for MeshPart
.
In fact, this is valid code:
That’s because
NegateOperation
and Seat
inherit from PVInstance
.
Ever wondered why every single object/class has the Clone()
, Remove()
, WaitForChild()
, FindFirstChild()
, etc. methods, and ClassName
, Name
, Parent
, etc. properties?
That’s because Instance has those, and every single object/class inherits from it:
However, are you a curious boy/girl and tried to create it with
Instance.new()
?You wonder why it doesn’t give an option for
BasePart
, Instance
, GuiObject
, etc.?That’s because those are abstract. They can inherit properties and methods, and create new ones for their children, but they aren’t used for actually doing stuff.
4: Type operators
Luau has these special characters that interact with how the type checking works:
-
|
- Union: When you want to give a variable/parameter multiple acceptable types; -
&
- Intersection: When you wanna join two types together (only works for custom types, so it’s useful for table custom types); -
?
- Short Union/nullable nillable? : You can put it after the type notation when you don’t know if it will be used every call to indicate that it can be either that type ornil
; -
::
- Assertion: Used for when you wanna assert (assign a type in an expression) a type;
Union examples:
-- local variable: type | type | type ...
local typeChecking: string | number = "Can be either a string or number!"
typeChecking = 157
local typeChecking: boolean | nil = true
typeChecking = nil
local typeChecking: number | string | boolean = "This can be a number, string or boolean!"
typeChecking = 157
typeChecking = false
local typeChecking: nil | string | boolean | number = nil
typeChecking = "This can be nil, a string, a boolean or a number!"
typeChecking = 157
typeChecking = true
local typeChecking: boolean | number | nil = "This gives a type error though, because string wasn't noted"
Short union examples:
-- local type: type?
local probablyABoolean: boolean? = true
probablyABoolean = nil
local probablyAString: string? = nil
probablyAString = ""
local probablyNil: nil? = nil -- This is useless, but by logic it is valid
local numberBooleanOrNil: number | boolean? = nil
numberBooleanOrNil = 157
numberBooleanOrNil = true
local probablyANumber: number? = 157
probablyANumber = nil
probablyANumber = true -- Type error, only accepts a number or nil!
Assertion examples:
-- expression( (variable :: type) )
-- If we assign type any to it, the type checker's not gonna know it is a number
local x: any = 1
-- So it's gonna scream about this, because it doesn't know if x is a number
print(x + 1)
-- Not if we assert it is a number, then it's going to be gud
print( (x :: number) + 1 )
5: Type checking in functions
For type checking in functions, follow the formula from before: variable: typenotation
.
However, as you may know, functions can return stuff. For that case, you put a colon after the two brackets, which indicates the type of what is returned: local function doStuff(): string
- that’d return a string.
local function addNumbers(num1: number, num2: number): number
return num1 + num2
end
addNumbers(1, 2) -- 3!
addNumbers("a",2) -- Oops, type error, I gave it a string, but it's only accepting a number!
local function somethingICameUpWith(): number
return "Type checking rn: u stooped bro?"
-- Must return a number!!!!
end
local function _157BestNumber(): string
return "This is good though!"
end
How about we use type operators too?
-- Never do this in a real game, be smart and assign "stringToBeConverted" to the string type!
-- The assertion at return is needed though
local function canToNumber(stringToBeConverted: any): boolean | number
-- tonumber() returns nil if it isn't be converted to a number, else, returns the number.
-- You saw how we assigned any to stringToBeConverted? We can use
-- the assertion operator to make Luau not scream about it
-- number? as it can be a number or nil
local result: number? = tonumber( (stringToBeConverted :: string) )
if not result then return false end
-- Asserted as it's gonna scream "boolean | number cannot be converted to number | nil (number?)"
return ( result :: number)
end
canToNumber("157") -- 157!
canToNumber("am i funny? answer true pls") -- False!
6: Type checking in loops
As you might have figured out, everything that is a variable can have type notations. That includes stuff that has parameters, such as functions, loops, events, and so on.
How about a for i, v in pairs()
loop that is so hot it makes you immediately want to type check everything in your games?
-- Assume this folder is full of Part's
local parts = game.Workspace.partsFolder:GetChildren()
for partIndex: number, part: BasePart in pairs(parts) do
print(partIndex.." "..part.Name)
-- 1 Part
-- 2 Part
-- 3 Part
-- etc.
end
-- What about a dictionary with key strings and numbers?
local tab = {
of = "i",
[1] = 751,
course = "love",
[2] = 157,
i = "type",
[3] = "checking",
_do = "!"
}
-- This stuff is nice to look at
for key: string | number, value: string | number in pairs(tab) do
print(key.." "..value)
-- of i
-- 1 751
-- course love
-- 2 157
-- i type
-- 3 checking
-- _do !
end
You can also type check regular for loops:
for count: number = 10,1,1 do
print(count)
-- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
end
7: Type checking events
With our knowledge you can type check your event parameters to make them extra spicy!
-- We can use the Player object/class as the type for the player parameter
game.Players.PlayerAdded:Connect(function(player: Player)
print(player.UserI) -- Key UserI not found in class "Player"!
print(player.UserId)
end)
local stuffToBuy = {
coolThing = 100,
}
-- Player class/object for player parameter, string for nameOfThing parameter
remote.OnServerEvent:Connect(function(player: Player, nameOfThing: string)
if not stuffToBuy[nameOfThing] then return end
if player.cash.Value < stuffToBuy[nameOfThing] then return end
-- Just an example
player.cash.Value -= stuffToBuy[nameOfThing]
player.inventory[nameOfThing].Value = true
end)
8: Custom types!!!
This is a bit complicated, but it’s definitely worth it to learn!
I’ve mentioned earlier you can make custom types. That’s right, you can!
To define a type, you use the type
keyword, followed by the name. Then, the type of the our new type. type typeName = typeType
For example, we can make a shortened version of the default types:
type num = number
type str = string
type bol = boolean
type nl = nil
local function compareNumber(num1: num, num2: num, maybeNum3: num | nl): bol
local message: str = "OwO what's this?"
return num1 == num2
end
But that’s kinda useless, it’s like overwriting a global roblox function with a local one.
So let’s use custom types to it’s full power!
9: Custom table types
I’d recommend MEMORIZING stuff about tables in the article in point 0.1.
So, you probably are asking yourself right now: “How can I make a custom type for a table, when all you can do is set them to existing types?”
Well, perhaps you read point 1 properly and knew that table
is a Vanilla Lua type. You know the table constructor is {}
? That’s how we also define table types.
There are 3 formulas for a tables: one that you can have as much keys and values while keeping the type checking:
-- type typeName = { [keyTypenotation] : valueTypeNotation }
-- This type will only accept a string as a key and a string or boolean as a value
type tab = { [string] : string | boolean }
local tableTest: tab = {
haha = true,
lol = false,
aaa = "This is really fun",
[1] = "WHAT IS THIS? THE KEY IS SUPPOSED TO BE A STRING!!1",
}
One that you predefine every key name and value:
-- type typeName = { keyName : typeName }
type tab = {
-- If no key name is specified, the key is a number, else, a string
firstSubTable : {
num1: number,
num2: number,
bol1: boolean,
bol2: boolean,
},
secondSubTable : {
something: boolean?,
funnyName: number | string
},
aCertainValue: boolean | number
}
-- The table has to STRICTLY follow the names of keys (and types of course)
local sumTable: tab = {
firstSubTable = {
num1 = 1,
num2 = 2,
bol1 = true,
bol2 = false,
},
secondSubTable = {
something = nil,
funnyName = "very funny indeed",
},
aCertainValue = "haha string go brrr" -- Nooo!!1! You can't just make it a string!!!!1 It's supposed to be a boolean or number!!!!!11
}
And one that you can give arguments to (can be both of the first 2 formulas):
-- The arguments are between <>, separated by comma
-- As much keys and values as you want:
type defineTheTableTypeYourself<keyType, valueType> = {
-- We gave string and number, so the key can be a string and the value a number or string
[ keyType ] : valueType | keyType
}
local a: defineTheTableTypeYourself<string, number> = {
funny = 6,
number = 9,
[1] = "The key is a number! Type error!"
}
-- Predefined table
type predefinedTable<typeOfValue> = {
thisIsGodTier : {
val1: typeOfValue,
val2: typeOfValue,
},
reallyCool: typeOfValue
}
local tab: predefinedTable<string> = {
thisIsGodTier = {
val1 = "Bery gud",
val2 = "Bery noice"
},
oopsWrongKeyNameAndType = true
}
Remember when I said to sometimes use the type in the same line the variable is initialized? It’s useful to do that with table types. It’ll be SUPER useful when type intellisense comes out
type Array<typ> = { [number] : typ }
local testArray: Array<string> = { "Very", "Noice!" }
Last but not least, let’s use the intersection operator to recreate the Vector2 and Vector3 datatypes as types i swear i didn’t steal the example from the official page
type x = {x: number}
type y = {y: number}
type z = {z: number}
type Vector2 = x & y
type Vector3 = x & y & z
local vec2: Vector2 = {x = 157, y = 157}
local vec3: Vector3 = {x = 157, y = 157, z = 157}
local illegalvec3: Vector3 = {
x = 157,
y = "This is a string, but the type y which Vector3 is intersecting with needs to be a number",
true -- Same here, it needs to be a number, not a boolean
}
10: How to organize your types
Once you become an official Lua god and start modularizing your code, and you’re gonna need those types, how will you make it organized and efficient (not copy pasting 10 types at the top of all scripts)?
Of course module scripts! (after all, it’s modularized)
There is this really funny keyword called export
and when inside a ModuleScript, it makes the type as a value inside the table the module returns, under the type name.
Here's my system for example
local Types = {}
-- Services
local rs = game:GetService("ReplicatedStorage")
-- Folders
local clientModules = rs:WaitForChild("modules")
local clientClasses = clientModules:WaitForChild("classes")
-- Modules
local ConnectionManager = require(clientModules:WaitForChild("ConnectionManager"))
local PurchaseManager = require(clientModules:WaitForChild("PurchaseManager"))
-- Types
export type PlayerData = {
stats : {
cash: number,
exp: number,
typingSpeed: number
},
items : { [string]: { [string] : string } | { [string] : { [string] : string } } },
values : {
firstTime: boolean,
savedTimes: number,
}
}
export type UserData = {
[number] : PlayerData
}
export type ConnectionManagerTyp = typeof(ConnectionManager.new())
export type PurchaseManagerTyp = typeof(PurchaseManager.new())
return Types
Inside a script, I can require and do like so:
-- Modules
local Types = require(clientModules:WaitForChild("Types"))
-- Types
type PlayerData = Types.PlayerData
type UserData = Types.UserData
11: Conclusion
Use type checking when you can, you won’t regret it!
Thanks for reading my first tutorial and I hope you learnt something!\( ̄▽ ̄)/