Type checking for beginners!

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


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 is number
  • Strings: "Type checking", "is awesome!"; (text), type is string
  • Booleans: true, false, or nil, which is the same as false ; (truthy or falsy), type is boolean
  • 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 is nil
  • 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…image
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

image
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

image
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? :thinking:
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?? :thinking:
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:
image

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? :thinking:

  1. RemoteEvent is the same as Part, so it’s gud ( ´ ω ` )
  2. No lol u stooped??!!1 (・`ω´・)


It’s number 2!
image

What about this example?

local part: BasePart
part = Instance.new("Part")
part = Instance.new("MeshPart")
  1. What sorcery is this??? Of course it’s wrong! (#`Д´)
  2. The image from the wiki shows Part inherits from BasePart, and I also saw by my own that MeshPart does too. (︶▽︶)

Number 2 is correct again!
image

What??? image
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:
image
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()?
image
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 or nil;
  • :: - 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

image


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
	
}

image
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!\( ̄▽ ̄)/
image

329 Likes

tldr;
remember what colour’s what

7 Likes

Wow! I didn’t realize Lua had this! For anyone interested, this is a great concept for people interested in learning how to code Apple apps! In Swift, the language Xcode uses, the first time you declare a variable, you can not change what type it is! (Xcode is Apple’s app coding app.)

4 Likes

It has been out of beta for a couple of months already!
Regarding the type checking concept, every OOP oriented language is technically type checked (and they are also typed), since when declaring a variable you specify what type it is (and if you don’t specify, it will only accept the type it has been initialized with). And languages that are un or weakly typed, such as JavaScript, have typed versions, as TypeScript.
I think Luau type checking overall teaches you about typed languages in general (including Swift)!

4 Likes

i might be asking this to the wrong person but any idea why we can’t type check functions?

2 Likes

Well, what would you check them for? Can you elaborate?

2 Likes

I don’t fully get what you’re talking about? But if I understood you correctly, have you read point 5 “Type checking in functions”?

2 Likes

so, i’m kinda new to this type checking stuff but let’s say i’m trying to have a function return a function that returns a string or something

local foo = function(thing: string): function --<
	return function(otherThing: string): string
		print(thing, otherThing)
		return ''..thing..otherThing..''
	end
end

or maybe i’m trying to make an array that only takes functions:

type FunctionArray = {[number]: function} --<

or you have already have said array and it’s full of functions and you want to activate (for lack of a better term (or rather the right term lol)) all of them via pairs loop

for index: string | number, fn: function in pairs(functions) do --<
	fn()
end

just wanted to know why it can’t just work like that?

2 Likes

function can’t be used as a type because it is the keyword used for declaring functions. The interpreter’s going to scream at you if you try to do so:
image
It’s telling you that it was expecting a type, but got a function instead (not the function type)
HOWEVER, it has been mentioned that you can have predefined function types, and then you don’t have to type check functions that have been noted with the type:

-- Between the () are the argument types, after -> is the return type
type fun = (string, number) -> string

local test: fun = function(testString, testNumber)
	return testString .. testNumber
end

However, I didn’t include it in the topic because it doesn’t really work…


But nonetheless you can use that to type check function types.

16 Likes

Bumping the topic as I updated it with more information and the brand new assertion operator ::!

4 Likes

First off:

You duplicated the section “9”

Second, what’s the benefit to type-checking? Isn’t it much simpler to have versatile variables? There’s some cases where you want type function to change to type nil etc. Even disregarding those situations, this just takes up time. What’s the benefit?

5 Likes

Fixed!


source
TL;DR: performance-wise, no benefits. yet!
On the convenient side (which I think is the best part), you are type safe. No more type errors because you gave a function on line 20 an argument of wrong type on line 500, or gave a library function wrong arguments, or perhaps you want intellisense for an object! this is why 60% of JS developers switched to TS, and 22% want to, when it doesn’t even have performance benefits as it just compiles straight to JS
Or when you don’t remember the methods, events or properties of an object, instead of switching to a browser, searching for the class, opening up the devhub, scrolling to find the method/property/event, then clicking on it to see what it does/how to use it, type checking allows you to have intellisense so you can quickly see everything the object has type intellisense isn’t out yet on Studio, but you can use VSC (a way better editor) with the Roblox LSP extension to get it

You probably aren’t typing idiomatic code. But if you actually wanna do that, if I was you (not trying to compare), I’d gladly sacrifice 5 seconds of my time to add the type union!

The time you save with the convenience benefits definitely outweighs adding type notations! (^▽^)

11 Likes

A very good tutorial, really explains type checking very well for beginners.

3 Likes

This is a really helpful tutorial and I’m sad I didn’t know of its existence sooner. Lua(u) is the only language I’ve ever programmed in so I’ve missed out on all the goodies (and annoyances?) of other languages. Typechecking has been out for a while but I’ve only ever known how to define the types of arguments and returns of functions, nothing else.

This dumbs down the content really easily since I couldn’t understand the typechecking documentation too well due to my unfamiliarity with types and all. I really wanted to incorporate typechecking for cleaner code, the potential future improvements and to leverage script analysis (especially in VS Code) but since I couldn’t grasp it I ended up shelving it and didn’t feel motivated to revisit it. Now I can start investing more time into applying typechecking in current and future code I write.

Thank you for the writeup!

9 Likes

Excuse me if I’m wrong since I’m only just learning about type checking, but couldn’t the issue in your example be fixed by adding the ? operator to the type number?

type fun = (string, number?) -> string

if I’m not mistaken, that should allow you to use the types number and nil in place of the function’s second argument.

2 Likes

Yes, that’d fix it. But with that post I made is to show that noting the types in the custom function type didn’t propagate to functions noted with it.
With the new type-on-hover feature, it appears it makes it generic:
obraz

(and it infers the return type from the body of the function, instead of the type too)

Oh yeah and I need to update this tutorial :sweat_smile:

2 Likes

I found a way and its quite hacky and might not be needed but you can by using typeof(function() end)

3 Likes

Is there a way to type check for metatables? Currently, I don’t have a solution on how it works.

These are some ways I tried, to no avail
type met<t, mt> = typeof(
    setmetatable(t, mt) -- Unkown global 't', Unkown global 'mt'
)
type met = typeof(
    setmetatable(
        {},
        {__index: typeof(function() end)} -- Unkown global '__index'
    )
)
type t = {}
type mt = {
	__index: typeof(function() end)
}

local foo: typeof(setmetatable(t, mt)) = setmetatable( -- Unkown global 't', Unkown global 'mt'
	{},
	{
		__index = function()

		end,
	}
)
type t = {}
type mt = {
	__index: typeof(function() end)
}

local foo: t & mt = setmetatable( -- Type 'foo' could not be converted into type 'mt'
	{},
	{
		__index = function()

		end,
	}
)

Thank you for any help!

2 Likes

You forgot about the self parameter in methods.

type OOPType = {
    Print: (OOPType, string) -> nil
    -- The type must be declared as first parameter to consider the use of "self"
}

If you don’t write the type inside the function declaration as the first parameter, the linter will throw a warning saying "This function does not take self, did you mean to use ."

2 Likes

He didn’t forget anything. That’s not a class and it’s not called with self.

3 Likes