Intro to Typechecking

Learn how to use typechecking to make your scripts easier to read debug!

by Joshument

Author Avatar

Typechecking is a feature of many programming languages that verifies a certain type is what it says it is. For example, making sure that you are not assigning a boolean to a value that was originally a number. This has many benefits, most notably 2 things:

  1. Values that you send and recieve using functions can be verified to be what you want them to be
    
  2. Is able to tell you before you run the script whether or not there is a possible chance of error.
    

You may not know this, but Luau, the Roblox-designed superset of Lua (and what is used in Roblox Studio), has a typechecking feature! This is turned off by default, but it's quite easy to turn on and start using. Why use typechecking in roblox studio? Take the following function:

local function doSomethingToAPlayer(plr)
    print(plr.UserId)
end

Seems simple enough, right? But there's actually a pretty glaring issue with this piece of code. It works fine if I pass in a player object:

doSomethingToAPlayer(game:GetService("Players"):WaitForChild("Joshument"))

But what happens if I pass in soemthing like a number?

doSomethingToAPlayer(1)

In this situation, we'd get an error like this:

Workspace.Script:2: attempt to index number with 'UserId'

Although this gives us some idea of what's going on with our code, it has some issues. Most importantly, if this were a larger script, it may be difficult to find where the piece of code passing in a number is located. This sounds like a job for typechecking!

To start us off, let's first add this one-liner to the top of our script:

--!strict

This code sets the typecher to strict, as opposed to the default that doesn't do much at all. When you add this piece of code to your script, you may see your code being underlined when you try to call the function with a number:

Type 'number' could not be converted into '{- UserId: a -}'

This is huge! The code can now underline the origin of error within this code. Luau can automatically infer that the type being passed is a number, which does not have a field named UserId. Just with one special comment! In some cases, your code may not be able to determine by itself what kind of type it's supposed to be, or you may want to be more explicit on your types when you want to look at your code later. For this situation, you can use this syntax:

variableName: variableType

In the case of function defenitions, a type needs to be declared to give a concrete type, but you can see how in some cases it can still infer. In the case of our function, we could write it as so:

local function doSomethingToAPlayer(plr: Player)
    print(plr.UserId)
end

Explicitly naming your types also helps the autocompletion know what's available to it (due to you telling it the type), so you don't have to perfectly memorize the names of parameters. I reccommed getting into the habit of using this.

We can also use this syntax on a function name to state what it returns, which will make the return type verbose. I also suggest doing this as it's much easier to read. (note that things that return nothing are different than nil, in those situations do not explicitly declare you returning nil or you may get yelled at by the typechecker):

function functionName(parameterName: parameterType): returnType

Luau comes with a few primitive types, as well as some special types. The primitive types are nil, string, number, boolean, table, function, thread, and userdata. Note that this does not include tables and functions, which are typed based on their contents.

Note that the names of types that Roblox has made are the same as the ClassName of the object.

Luau also includes some special types that can be used:

  1. unknown This refers to a type that could be any value, but cannot be used as them. You can use this type to check the type of a variable by using typeof().
  2. any This is the same as unknown, but can be used as any type. This is default when --!strict is not enabled as well. (so yes, I lied, it is technically on :3).
  3. never I don't think you will ever see or use this (I haven't), but it refers to a type that is not any value. I mean literally none, nothing will ever have this type, ever.

You are also able to combine types using other syntax:

-- this variable could be any of the types specified, seperated by
unionType: type1 | type2 
-- this variable represents all of the types specified.
-- note that this does not work for primitives, and is mostly used for joining tables.
intersectionType: type1 & type2
-- this variable represents exactly what the value is at runtime, specified by the type.
singletonType: "Stuck Forever" = "Stuck Forever"
stringType = singletonType -- ok
singletonType = "Change me!" -- not ok

Functions and tables can be defined as so:

local myCoordTable: {x: number, y: number, z: number} = {1, 1, 1}
local isAboveTen: (int) -> boolean = function() 
	return int > 10 
end

You can make your own custom types by using the type keyword. This avoids redundancy:

type XYZCoords = {x: number, y: number, z: number}
local myCoordTable: XYZCoords = {1, 1, 1}

In the situation where a value isn't instantly created (i.e. it's made from another script) you might get issues like this:

type 'Instance' could not be converted into <someType>

In situations like this, we can use the following syntax to convert a type into another type:

someValue :: someType

For example, if we got an Instance, and we wanted to make it an IntValue, we could write it the following way:

local IntVal = SomeInstance :: IntValue

Don't worry about naming your variables the same name as types, as Luau can distinguish between the two, disallowing conflicts based on names. Be cautious when using this syntax though, as it runs on the assumption that you know the value is going to be what you say it is. In some cases though, this is the only real option, so do not be shy when using it.

Types may also be nil. By default, the type checker assumes the value will always be valid (and not nil), and therefore may give you a bit of heck if you try to access it when it might be nil (though setting it is fine). You can tell luau that an object might be nil by adding a ? to the end of the type.

local maybeNil: number?

In situations like these, however, you will have to assert that the value is true. There are two ways to do this:

  1. assert that the value is not nil this works fine when the value is said to be nil and you know that it will always be valid. But note that this will error if the value is nil:
assert(maybeNilValue)
  1. Use an if statement to conditionally apply a default value tends to work the best, and I would prefer this solution over the former unless you are unable to do so:

local myVariable: Instance

do
    local temp  = pretendThisMightReturnNil()

    if temp then
        myVariable = temp
    else
        warn("variable was nil!")
        myVariable = someDefault
    end
end

The typechecker is usually able to infer that it's impossible for the value to be nil after running it through simple tests like this.

This should be most of the knowledge that you need to know for typechecking! There's a lot more to this, so I implore you to do your own research if you want to. I obviously can't explain everything in one tutorial (i.e. generics and typepacks), and this is getting quite long, sorry :( Hopefully this helps you write better code!!

View in-game to comment, award, and more!