Luau supports a gradual type system through the use of type annotations and type inference. These types are used to provide better warnings, errors, and suggestions in the Script Editor.
Defining a Type
Use the type keyword to define your own types:
type Vector2 = {x: number, y: number}
Inference Modes
There are three Luau type inference modes that can be set on the first line of a Script:
- --!nocheck - Don't check types
- --!nonstrict - Default mode for all scripts, only asserts variable types if they are explicitly annotated
- --!strict - Asserts all types based off the inferred or explicitly annotated type
The default mode for the type checker is --!nonstrict. The other two modes control how strict the type checker is with inferring and checking types for variables and functions. Any type mismatches in scripts are highlighted in the Script Editor and surfaced as warnings in the Script Analysis window.
Types
A type annotation can be defined using the : operator after a local variable, followed by a type definition. By default, in nonstrict mode, all variables are assigned the type any.
local foo: string = "bar"local x: number = 5
There are four primitive types that can be used in an annotation:
- nil - no value
- boolean - true or false
- number - a numeric value
- string - text
Within Roblox, all classes, data types, and enums have their own types that you can check against:
local somePart: Part = Instance.new("Part")local brickColor: BrickColor = somePart.BrickColorlocal material: Enum.Material = somePart.Material
To make a type optional, use a ? at the end of the annotation:
local foo: string? = nil
This will allow the variable to be either the specified type (in this case string) or nil.
Literal Types
You can also cast strings and booleans to literal values instead of using string and boolean:
local alwaysHelloWorld: "Hello world!" = "Hello world!"alwaysHelloWorld = "Just hello!" -- Type error: Type '"Just hello!"' could not be converted into '"Hello world!"'local alwaysTrue: true = false -- Type error: Type 'false' could not be converted into 'true'
Type Casts
Sometimes, you might need to assist the typechecker by explicitly casting a value to a different type with the :: operator:
local myNumber = 1local myString: stringmyString = myNumber -- Not OK; type conversion errormyString = myNumber :: any -- OK; all expressions can be cast to 'any'local myFlag = myNumber :: boolean -- Not OK; types are unrelated
Function Typing
Consider the following function:
local function add(x, y)
return x + y
end
This function adds x to y, but errors if one or both of them is a string. Luau doesn't know that this function can only use numbers. To prevent this category of problem, add types to the parameters:
local function add(x: number, y: number)
return x + y
end
Luau now knows that the function takes two numbers and throws a warning if you try to pass anything that isn't a number into the function:
add(5, 10)add(5, "foo") -- Type error: string could not be converted into number
To define a return type, put a : operator at the end of the function definition:
local function add(x: number, y: number): number
To return multiple types, place the types in parentheses:
local function FindSource(script: BaseScript, pattern: string): (string, number)
return 42, true -- Type errors
end
Defining a Functional Type
A functional type can be defined by using the syntax (in) -> out. Using the functions from the previous examples, the types of the functions are:
type add = (x: number, y: number) -> numbertype FindSource = (script: BaseScript, pattern: string) -> (string, number)
Table Types
Luau does not have a table type; instead, table types are defined using {} syntax. One way of defining tables is using the {type} syntax, which defines a list type.
local numbers: {number} = {1, 2, 3, 4, 5}local characterParts: {Instance} = LocalPlayer.Character:GetChildren()
Define index types using {[indexType]: valueType}:
local numberList: {[string]: number} = {Foo = 1,Baz = 10}numberList["bar"] = true -- Type error: boolean can't convert to number
Tables can also have explicit string indices defined in a type.
type Car = {
Speed: number,
Drive: (Car) -> ()
}
local function drive(car)
-- Always go the speed limit
end
local taxi: Car = {Speed = 30, Drive = drive}
Variadics
Here's a function that calculates the sum of an arbitrary amount of numbers:
local function addLotsOfNumbers(...)
local sum = 0
for _, v in {...} do
sum += v
end
return sum
end
As expected, this function can take any value, and the typechecker won't raise a warning if you provide an invalid type, such as a string.
print(addLotsOfNumbers(1, 2, 3, 4, 5)) -- 15print(addLotsOfNumbers(1, 2, "car", 4, 5)) -- Attempt to add string to number
Instead, assign a type to the ..., just like how you assign any other type:
local function addLotsOfNumbers(...: number)
And now, the second line raises a type error.
print(addLotsOfNumbers(1, 2, 3, 4, 5))print(addLotsOfNumbers(1, 2, "car", 4, 5)) -- Type error: string could not be converted into number
However, this does not work when writing a functional type definition:
type addLotsOfNumbers = (...: number) -> number -- Expected type, got ':'
Instead, use the syntax ...type to define a variadic type.
type addLotsOfNumbers = (...number) -> number
Unions and Intersections
You can even define a type as two or more types using a union or intersection:
type numberOrString = number | stringtype type1 = {foo: string}type type2 = {bar: number}type type1and2 = type1 & type2 -- {foo: string} & {bar: number}local numString1: numberOrString = true -- Type errorlocal numString2: type1and2 = {foo = "hello", bar = 1}
Defining an Inferred Type
You can use the typeof function in a type definition for inferred types:
type Car = typeof({Speed = 0,Wheels = 4}) --> Car: {Speed: number, Wheels: number}
One way to use typeof is to define a metatable type using setmetatable inside the typeof function:
type Vector = typeof(setmetatable({}::{x: number,y: number}, {}::{__add: (Vector, Vector|number) -> Vector}))-- Vector + Vector would return a Vector type
Generics
Generics are at a basic level parameters for types. Consider the following State object:
local State = {Key = "TimesClicked",Value = 0}
Without generics, the type for this object would be as follows:
type State = {Key: string,Value: number}
However, you might want the type for Value to be based on the incoming value, which is where generics come in:
type GenericType<T> = T
The <T> denotes a type that can be set to anything. The best way to visualize this is as a substitution type.
type List<T> = {T}local Names: List<string> = {"Bob", "Dan", "Mary"} -- Type becomes {string}local Fibonacci: List<number> = {1, 1, 2, 3, 5, 8, 13} -- Type becomes {number}
Generics can also have multiple substitutions inside the brackets.
type Map<K, V> = {[K]: V}
To rework the State object from earlier to use a generic type:
type State<T> = {Key: string,Value: T}
Function Generics
Functions can also use generics. The State example infers the value of T from the function's incoming arguments.
To define a generic function, add a <> to the function name:
local function State<T>(key: string, value: T): State<T>
return {
Key = key,
Value = value
}
end
local Activated = State("Activated", false) -- State<boolean>
local TimesClicked = State("TimesClicked", 0) -- State<number>
Type Exports
To make it so a type can be used outside of a ModuleScript, use the export keyword:
Types Module in ReplicatedStorage
export type Cat = {Name: string,Meow: (Cat) -> ()}
Script Using the Types Module
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Types = require(ReplicatedStorage.Types)
local newCat: Types.Cat = {
Name = "metatablecat",
Meow = function(self)
print(`{self.Name} said meow`)
end
}
newCat:Meow()