Typechecking: From Zero to Master | TUTORIAL

Hello community :wave:

I made this topic to help those who want to enter the world of typechecking, but do not know where to start or how, and also for those who have doubts about some topics. I hope this topic helps you! If so, I would like you to like this topic so that it can reach more people and help more developers!

Why read this Topic?

The reasons why you should read this topic are:

  • Explanations with code examples
  • Reasons why certain concepts are useful will be given.
  • Sources are given with which you can continue to learn Typechecking on your own
  • You can contribute to the Topic by correcting explanations or information to always have updated and real information!

:warning: Some topics may be explained wrongly or incorrectly, I would appreciate a correction in the comments to avoid misinforming the community. :warning:


Topics to be discussed in this Topic

  • What is typechecking?
  • Type inference in LUAU
  • Type annotation
  • built-in types (primitive types)
    • string
    • number
    • boolean
    • table
    • function
  • Optional types (? operator)
  • unknown, any and never
  • How to type a table and dictionaries
  • Index signatures (index types)
  • Type aliases (type keyword)

These topics will be added in the future (i’m tired)

  • Function types (functional types)
  • Union types
  • Union types with built-in types
  • Intersection types
  • Singleton types (literal types)
  • Type casting (:: operator)
  • Type inference with typeof method
  • Type inference modes (nocheck, nonstrict, strict)
  • MORE COMING SOON

Click on the dropdowns to see the content of each section

1. What is typechecking

1. What is typechecking?

Let’s imagine you’re baking a cake. Each ingredient you use, like flour, sugar, or eggs, has a specific role in the recipe. Now, if you mistakenly use salt instead of sugar, the cake won’t turn out right.

In programming, each piece of data (like numbers, text, or more complex data structures) is like an ingredient. They have a “type” (like number, string, etc.) that determines what you can do with them.

Type checking is like the kitchen assistant who checks your ingredients before you add them to the mix. If you’re about to add salt where the recipe calls for sugar, type checking raises an alarm, saying “Hey, you can’t use that here!”

In summary, type checking is a crucial aspect of programming that contributes to error detection, code reliability, optimization, documentation, and abstraction. It’s like a safety net, helping to catch and prevent potential issues in your code.

2. Type inference in LUAU

2. Type inference in LUAU

First of all: what is type inference?

Type inference is LUAU’s way of “GUESSING” what type of data a variable or an expression is. This is useful for example for autocompletion, but there are times when LUAU does not know what type of data something is, so we will have to help LUAU to know what type of data something is.

Example:
type-inference

LUAU can not only infer the data type of a literal value, but it can also infer the data type of an expression!

expression-inference

Now knowing what type inference is, we can move to the next section :slight_smile:

3. Type annotation

3. Type annotation

Type annotation is a way of STRICTLY indicating the data type of a variable.

The Type Annotation will be used to type a variable with a specific data type, for security or auto-completion purposes.

How to use Type Annotation?

To use the Type Annotation, it will be done with the : operator. In this way:

local variable: data type = value of that data type

Actual use of Type Annotation for autocompletion

local player = game.Players.LocalPlayer
local character = player.Character
local humanoid: Humanoid = character:WaitForChild("Humanoid")

humanoid.WalkSpeed = 12

And what happens if you pass another type of data to the one specified in the variable?

What will happen is that LUAU will give us a warning, telling us that we cannot pass a data type other than the one we specified. Example:

type-annotation

IT IS IMPORTANT TO KNOW THAT ROBLOX WILL NOT GIVE AN ERROR IF WE EXECUTE THE CODE, THIS IS ONLY USEFUL TO AVOID BEHAVIORS IN THE CODE THAT WE DO NOT WANT.

We can also use data types that Roblox has already made for us! For example the data type Model.

local someModel: Model = workspace.AmongUs
--[[
This is not so useful since roblox will not know what objects are
 inside that model or if objects even exist inside it, so it is not the best
 use of this type of data.
]]
4. built-in types (primitive types)

4. built-in types (primitive types)

Primitive types are data types that come with the LUA language, and are the simplest and most commonly used data types.

These data types can be used to write variables, function parameters or table properties.

Example of how to type variables with these primitive data types:

--[[
Most common primitive data types:
    string, number, table, function, boolean
]]
local thisIsAString: string = "hi"
local aRandomNumber: number = 53952
local aBoolean: boolean = true -- or false
local anEmptyTable: {} = {}
local aFunction: () -> () = function() end

5. Optional types (? operator)

5. Optional types (? operator)

The ? operator is used to indicate that a variable can be of the specified data type or nil. And in the case that we put another type of data different from the one we specify, LUAU will give error in the same way, but if we pass nil, it will not give any warning.

optional-type

This operator can be used to indicate that a function can receive a parameter of a data type, or it can not receive it. For example:

local function foo(a: number, b: number?) -- b is optional
	b = b or 10 -- assigning a default value in the event that a value is not passed
	
	print(a, b)
end

foo(1, 4) -- correct, no warnings
foo(1) -- correct, no warnings (If the optional type operator were not used, this would result in an error)

To use this operator, you must add ? at the end of the specified data type

6. unknown, any and never

6. unknown, any and never

any

The any type in Luau is similar to unknown, but it’s used when the type inference falls back to a default type. It’s not a type that you would use in type annotations. Instead, it’s a type that Luau uses internally during type inference.

Also note that “any” can represent ANY type of data, be it a number, string, boolean, table, ect. And because of this, luau will let us perform any kind of operation with this data type without having to check the data type of the parameter.

-- it is not necessary to set `:any`, if not set, luau will infer the parameters as `any` anyway.
local function addNumbers(a, b)
	return a + b -- No warnings
end

Cases in which any may appear

  • When the data type of a parameter is not specified
  • When the return type of a function (e.g. WaitForChild) is unknown.
  • When it is used strictly (when we put it in some variable or parameter)

Unknown

Unknown is a data type similar to ANY, the main difference is that this data type will not let us perform operations or use methods until we know what its data type is. This datatype is often used in remote events as a small layer of security.

To find out what the data type of a unknown parameter (or variable) is, we can use the typeof or type function.

Basic example:

-- without checking the data type
local function addNumbers(a: unknown, b: unknown)
	return a + b -- warning: type unknown could not be converted into number
end

-- checking the data type
local function addNumbers(a: unknown, b: unknown)
	if type(a) == "number" and type(b) == "number" then
		return a + b
	end
end

Because LUAU already knows that “a” and “b” are numbers, then it will let us perform the operations that numbers allow. This can also work for any type of data.

never

The never type in Luau represents a value that never occurs. It’s used in situations where a function never returns a value. For example, a function that always throws an error could have a return type of never.

function throwError(): never
    error("This function always throws an error")
end

In this example, the throwError function never returns a value because it always throws an error, so its return type is never.

Summary:

  • any: The data type that represents ALL possible data types (we can perform operations with this data type without receiving warnings).
  • unknown: A data type similar to “any” but more useful and powerful. This datatype will not let us perform operations on the value until we know what type of datatype it is.
  • never: A data type used to indicate that something will never be executed or stops the execution of the current code.
7. How to type a table and dictionaries

7. How to type a table and dictionaries

Knowing how to type tables and dictionaries is important to establish mandatory properties with a specific data type for each property. Also knowing how to type tables and dictionaries will help us to have better autocompletion in these.

If a table is not correctly typed, it will be inferred by LUAU by giving the data type “any” to each property or element. And let’s remember to always avoid typing our stuff with “any” for code safety.
inferred-table
any-inferred
:warning: The tables or dictionaries will be inferred correctly if we use the strict mode, but we will not see this in this Topic until a future time :warning:

Syntax for typing tables and dictionaries

Tables

In order to type tables, we can use the literal syntax (using {}) to do so. Inside the {} we can put any datatype we want the table to contain. For example:

local myTable: { dataType } = {  only values of the specified data type }

-- Example:
local myTable: { string } = { "hi", "bye", "like" }

In the previous example: {string} means that we want that table to contain only any number of strings as content. If we pass another type of data, LUAU will give us a warning.

local myTable: { string } = { "hi", "bye", "like" } -- Correct!
local myTable2: { string } = { "hi", "bye", "like", 20, true, false } -- Nuh uh, incorrect

Not only can we use string, we can use any data type, such as boolean.

local onlyBoolean: { boolean } = { true, false, false, true, true, ... }

Type nested tables

To type nested tables, we can do it using tables within tables as type annotation:

local nestedTableStrings: { { string } } = { { "hi" }, { "bye" } } -- Correct
local nestedTableStrings2: { { string } } = { "hi", "bye" } -- Incorrect

Typing tables in this way is not a good idea as it does not allow you to create complex data types. This can be fixed with index-types and type aliases (see next sections).


Dictionaries

In Lua, a dictionary is a data structure that holds key-value pairs. Each key in the dictionary is unique and is used to access its corresponding value. Here’s how you can define a dictionary:

local dict: { keyName: string } = { keyName = "hello" }

Nested Dictionaries

Dictionaries can also contain other dictionaries as values. This is known as nested dictionaries. Here’s how you can define a nested dictionary:

local dict: {
	innerDict: {
		property: string,
	},
	normalProperty: string,
} = {
	innerDict = { property = "hello" },
	normalProperty = "hello"
}

Type Checking

Luau provides strict type checking to ensure that the values in the dictionary match the expected types. If a value of a different type is assigned to a key, Luau will throw an error.

--!strict

local dict: { keyName: string } = { keyName = 20 } -- Error

However, if strict mode is not enabled, Luau will not throw an error even if the types don’t match.

local dict: { keyName: string } = { keyName = 20 } -- No errors
8. Index signatures (index types)

8. Index signatures (index types)

Index types in LUAU, also known as index signatures, allow you to create dictionaries where the keys are not known beforehand but the type of the values is known. This is particularly useful when you want to create a dictionary that can hold an arbitrary number of keys, all of which have values of the same type.

Here’s how you can define a dictionary using index types:

local dict: { [index: string]: string } = { key1 = "hello", key2 = "world" }

In this example, index can be any string, and the value corresponding to index will be of type string. This means you can add as many keys as you want to dict as long as the values are strings.

Index types are powerful because they provide flexibility. You don’t need to know all the keys at the time of dictionary creation. You can add keys dynamically at runtime, which is not possible with regular dictionaries.

However, with great power comes great responsibility. When using index types, you need to ensure that the values you assign to the keys match the expected type. If not, Luau will throw a type error.

Here’s an example:

--!strict

local dict: { [index: string]: string } = { key1 = "hello", key2 = 20 } -- Error

In this example, we’re trying to assign a number to key2 which is expected to be a string. This results in a type error.

9. Type aliases (type keyword)

9. Type aliases (type keyword)

Type aliases are a way to create your own data types and use them to type things.

They allow us to have more readability in our code and allow us to have more complex data types.

To create a type alias you must do it with the following syntax:

type DataTypeName = value

-- Example of a type alias
type Person = {
    name: string,
    age: number,
    hobbies: { string }
}

How can they be used to type?

We will simply use the same syntax as the type annotation. In this way:

type SomeType = { hi: string }

local variable: SomeType = { hi = "Hi!" }

Export Type Aliases

Something interesting and useful that we can do, is to create a separate ModuleScript in which we have all our type aliases created to be able to use them in any script.

In order to export a data type, we need to do it with the keyword export followed by the type alias. Example: export type Person = {}.

To be able to use the data types that we have exported, it is done in this way:

-- Types.lua (ModuleScript)

export type Person = {
    age: number,
    name: string,
}

return nil -- We do not need to return any specific value if we are only defining type aliases.
-- SomeScript.lua
local Types = require(some_path.Types)

-- Now if we want to use some specific type of data, we must do it in the following way:
local me: Types.Person = { age = math.huge, name = "Isaac" }

We can use any data type within the type aliases


Topic updates

18/02/2024 - 1.0.0
  • Topic published

Resources

luau-lang: Type checking - Luau
Roblox Studios Documentation: Type Checking | Documentation - Roblox Creator Hub
A 2021 post by @intelsyntax_enjoyer: Type checking for beginners!
Video by Suphi Kaner: https://youtu.be/eRVDL7xlN8g?si=sblphfns9ybNP0tE
Video by MonzterDEV: https://youtu.be/LVx4RhXp2Jw?si=d0XWyJdyLpI47FbY


I will be updating this Topic little by little, so if you don’t understand something, you can ask me in the comments to try to explain it.

:star: Contribution :star:

Help correct misspelled things, content ideas or report poorly explained things to appear here!

Spelling Corrections: @King_GamingRobloxYT
Topic Corrections: @GameInspectors (category corection)

32 Likes

I may have left something empty, sorry if that’s the case, but I’ve already updated the any, unknown and never section :slight_smile:

2 Likes

I think ModuleScripts must return exactly one value.

2 Likes

(I understood what you meant, I expressed myself wrong, I’ve already modified it, thanks!)

1 Like

I LOVE TYPECHECKING. :money_mouth_face: :heart_eyes:
You can ask my dearest Adrian why I still haven’t completed all of the programming for Larpknights, and this is the answer he will get from me if I do not immediately say that I have been doing schoolwork. In reality, it is both typechecking and slacking off. :money_mouth_face:

1 Like

I hope you can learn something from this topic, there is still a lot to add, stay tuned for updates :wink:

One thing I hadn’t known before was “export type”. I’ll be re-writing some scripts soon.

1 Like

I’m glad you have learned at least something new, if you have any doubts don’t hesitate to ask. :happy1:

“help” would be misspelt in my book. It would be “Help” since it’s the start of a sentence.

1 Like

I learned new things thanks!

1 Like

You are welcome! Right now I don’t have time to update the topic, but there will be many more topics coming soon, I hope you’ll stay tuned :wink:

1 Like

Finally, I understand what that export keyword does.

1 Like

I’m glad you understood! I will update this post shortly and add a lot more content :wink:

1 Like

Wait, when i put

local dict: { [index: string]: string } = { key1 = "hello", key2 = "world" }

into a module script i get an error: ServerScriptService.Scripts.AI.Main:1: Expected ‘]’ (to close ‘[’ at column 15), got ‘:’

i dont know whats going on here

2 Likes

The problem is that you are adding the text “index” in the square brackets, if you want to create index signatures, you only need to specify the data type of the key, you don’t need to put any text or name before:

local dict: { [string]: string } = { key1 = "hello", key2 = "world" }

If you have any further questions, don’t hesitate to let me know so I can help you!

1 Like

very good and informational post, this should have more views!

1 Like

Thanks for commenting! I hope this post can reach more people to learn this interesting luau feature

1 Like

Thanks so much! This was very useful, as trial and error and using the previous guides have been a bit confusing.
Although I seem to still have issues trying to properly code a type for this nested dictionary and I’m probably making a dumb mistake but can’t seem to fix this.

export type InteractAction: {[string]: string = {Enum.KeyCode,}}  = {
	Action = {
		Key:Enum.KeyCode,
		OK:boolean,
	}
}
1 Like

You can’t use type annotation on a type alias definition, if you want to use index-signatures, you will have to do this:

export type InteractAction = { [string]: { Key: Enum.KeyCode, OK: boolean } }

Now you can do:

local dict: InteractAction = {
   idk = {
       Key = Enum.KeyCode.F,
       OK = true
   }
   ...
}
1 Like