Skip to main content

Data Types

Learning Objectives

By the end of this lesson, you will be able to:

  • Identify and use all fundamental Luau data types
  • Understand the difference between nil and false
  • Work with numbers in various formats (decimal, hex, binary)
  • Create and manipulate strings using multiple syntaxes
  • Use string interpolation for dynamic text
  • Understand how types differ from JavaScript and After Effects

AE/JS Syntax Comparison

Before diving in, here's how Luau data types compare to what you may already know:

ConceptAfter Effects ExpressionJavaScriptLuau
No valueN/Anull / undefinednil
Booleantrue, falsetrue, falsetrue, false
Number42, 3.1442, 3.1442, 3.14
Text"hello""hello" or 'hello'"hello" or 'hello'
Array[1, 2, 3][1, 2, 3]{1, 2, 3}
ObjectN/A{key: value}{key = value}
String join"Hello " + name"Hello " + name"Hello " .. name
TemplateN/A`Hello ${name}``Hello {name}`
Key Differences from JavaScript
  • No null vs undefined: Luau has only nil
  • No separate integer type: All numbers are double-precision floats
  • Curly braces for arrays: Use {} not []
  • Different string concatenation: Use .. not +
  • Simpler interpolation: Use {var} not ${var}

The nil Type

nil represents the absence of a value. It's Luau's way of saying "nothing" or "not set."

local uninitializedVariable  -- This is nil
local explicitNil = nil -- Explicitly set to nil

-- Check if something exists
if uninitializedVariable == nil then
print("Variable has no value")
end

JavaScript Comparison

// JavaScript has TWO "nothing" types - confusing!
let a; // undefined
let b = null; // null
a === undefined // true
b === null // true
a == b // true (loose equality)
a === b // false (strict equality)
-- Luau has ONE "nothing" type - simple!
local a -- nil
local b = nil -- nil
a == nil -- true
b == nil -- true
a == b -- true (always straightforward)

Why nil Matters in Rive

In Rive Node Scripts, nil is commonly used with:

  • Uninitialized values: Before init() runs, fields may be nil
  • Optional parameters: Functions can have optional arguments
  • Missing table keys: Accessing a non-existent key returns nil
  • late() initializer: Returns nil before the engine provides the value
export type MyScript = {
health: number?, -- The ? means this can be nil
}

function init(self: MyScript): boolean
-- health might be nil if not set
if self.health == nil then
self.health = 100
end
return true
end

The boolean Type

Booleans represent logical values: true or false.

local isActive = true
local hasCollided = false

-- From comparisons
local isPositive = 5 > 0 -- true
local isEmpty = #"hello" == 0 -- false

Truthy and Falsy Values

Critical Difference from JavaScript!

In Luau, only false and nil are falsy. Everything else is truthy!

-- These are ALL truthy in Luau:
if 0 then print("0 is truthy!") end -- PRINTS!
if "" then print("empty string is truthy!") end -- PRINTS!
if {} then print("empty table is truthy!") end -- PRINTS!

JavaScript Comparison

// JavaScript falsy values (6 total):
if (false) {} // falsy
if (null) {} // falsy
if (undefined) {} // falsy
if (0) {} // falsy ← Different from Luau!
if ("") {} // falsy ← Different from Luau!
if (NaN) {} // falsy

// Common JS pattern that DOESN'T work the same in Luau:
let score = 0;
if (!score) {
console.log("No score"); // This prints in JS!
}
-- Luau falsy values (2 only):
if false then end -- falsy
if nil then end -- falsy

-- This behaves DIFFERENTLY than JS:
local score = 0
if not score then
print("No score") -- DOES NOT PRINT! 0 is truthy!
end

-- Correct Luau way:
if score == 0 then
print("Score is zero")
end

The number Type

All numbers in Luau are double-precision floating-point (64-bit). There's no separate integer type.

Number Literals

-- Standard decimal
local count = 42
local price = 19.99
local negative = -100

-- Hexadecimal (base 16) - useful for colors
local red = 0xFF0000 -- 16711680
local green = 0x00FF00 -- 65280
local blue = 0x0000FF -- 255

-- Binary (base 2) - useful for bit flags
local flags = 0b1010 -- 10
local mask = 0b11110000 -- 240

-- Underscores for readability (large numbers)
local million = 1_000_000 -- 1000000
local precise = 3.141_592_653 -- 3.141592653

JavaScript Comparison

// JavaScript numbers are similar
let count = 42;
let price = 19.99;
let hex = 0xFF0000;
let binary = 0b1010;
let bigNumber = 1_000_000; // ES2021+

// JavaScript also has BigInt (Luau doesn't)
let huge = 9007199254740993n; // No equivalent in Luau

Precision Note

Since all numbers are floats, very large integers may lose precision:

local big = 9007199254740992    -- Safe max integer
local tooBig = 9007199254740993 -- May lose precision!

Special Number Values

local infinity = math.huge      -- Positive infinity
local negInfinity = -math.huge -- Negative infinity
local notANumber = 0/0 -- NaN (not a number)

-- Check for special values
local x = 0/0
if x ~= x then
print("x is NaN") -- NaN is the only value that doesn't equal itself!
end

The string Type

Strings are sequences of characters for text data.

String Syntax Options

Luau offers three ways to create strings:

-- Single quotes
local single = 'Hello, World!'

-- Double quotes (most common)
local double = "Hello, World!"

-- Multi-line strings with [[]]
local multi = [[
This is a
multi-line string.
It preserves line breaks!
]]

Escape Sequences

local newline = "Line 1\nLine 2"     -- \n = new line
local tab = "Column1\tColumn2" -- \t = tab
local quote = "He said \"Hello!\"" -- \" = literal quote
local backslash = "Path\\to\\file" -- \\ = literal backslash

String Concatenation

Use .. not +

Luau uses .. for string concatenation, NOT + like JavaScript!

-- Luau concatenation
local first = "Hello"
local second = "World"
local greeting = first .. " " .. second -- "Hello World"

-- Numbers are automatically converted
local score = 100
local message = "Score: " .. score -- "Score: 100"
// JavaScript uses +
let greeting = first + " " + second;
let message = "Score: " + score;

String Interpolation (Backticks)

Luau supports string interpolation with backticks - this is the cleanest way to build dynamic strings:

local name = "Player"
local level = 42
local health = 85.5

-- Old way with concatenation (messy)
local old = "Welcome, " .. name .. "! Level " .. level .. " - HP: " .. health

-- New way with interpolation (clean)
local new = `Welcome, {name}! Level {level} - HP: {health}`

-- Both produce: "Welcome, Player! Level 42 - HP: 85.5"

JavaScript Comparison

// JavaScript template literals use ${}
let message = `Welcome, ${name}! Level ${level} - HP: ${health}`;
-- Luau interpolation uses {} (no $ needed)
local message = `Welcome, {name}! Level {level} - HP: {health}`

String Length

local text = "Hello"
local length = #text -- 5

-- Works with variables too
local message = "Rive is awesome!"
print(`Message has {#message} characters`) -- "Message has 16 characters"

The table Type

Tables are Luau's only data structure - they serve as arrays, dictionaries, and objects.

-- Array-style (sequential integer keys)
local colors = {"red", "green", "blue"}

-- Dictionary-style (named keys)
local player = {
name = "Hero",
health = 100,
position = {x = 0, y = 0}
}

-- Mixed (possible but not recommended)
local mixed = {
"first", -- Index 1
"second", -- Index 2
name = "mixed" -- Named key
}
Learn More

Tables are covered in depth in the Tables lesson. This is just an overview of the type.


The function Type

Functions are first-class values in Luau - they can be stored in variables, passed as arguments, and returned from other functions.

-- Function stored in variable
local greet = function(name: string): string
return `Hello, {name}!`
end

-- Call it
local message = greet("World") -- "Hello, World!"

-- Type of function
print(type(greet)) -- "function"
Learn More

Functions are covered in depth in the Functions lesson. This is just an overview of the type.


Type Checking with type()

The built-in type() function returns the type of any value as a string:

print(type(nil))          -- "nil"
print(type(true)) -- "boolean"
print(type(42)) -- "number"
print(type("hello")) -- "string"
print(type({})) -- "table"
print(type(function() end)) -- "function"

Runtime Type Guards

local function processValue(value: any)
if type(value) == "number" then
print(`Number: {value * 2}`)
elseif type(value) == "string" then
print(`String: {value:upper()}`)
elseif type(value) == "boolean" then
print(`Boolean: {if value then "yes" else "no"}`)
elseif type(value) == "nil" then
print("Got nil!")
else
print(`Unknown type: {type(value)}`)
end
end

Exercises

Exercise 1: Type Snapshot ⭐

Premise

When you bind data or debug scripts in Rive, you often need to confirm what type a value actually is. Using type() helps you catch mismatches early, before the wrong value is fed into a drawing or Input Path.

Goal

By the end of this exercise, you will define values of multiple types and print a compact verification summary.

Use Case

Imagine a script that reads data from different sources: a ViewModel value, a state flag, and a table of points for a path. You need to verify that each is the expected type before you build visuals. A quick type snapshot prevents debugging blind spots.

Example scenarios:

  • Verifying ViewModel properties before use
  • Checking whether a value is a table or a number

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_TypeSnapshot
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise1 = {}

function init(self: Exercise1): boolean
-- TODO 1: Set isActive to true
local isActive = false
-- TODO 2: Set count to 42
local count = 0
-- TODO 3: Set label to "Score"
local label = ""
-- TODO 4: Set points to a table with 3 numbers
local points = {}
-- Keep this as nil on purpose
local maybeNil: any = nil
local callback = function() end

local summary = `{type(maybeNil)},{isActive},{count},{label},{#points},{type(callback)}`
print(`ANSWER: {summary}`)
return true
end

function draw(self: Exercise1, renderer: Renderer)
end

return function(): Node<Exercise1>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Set isActive to true
  2. Set count to 42
  3. Set label to "Score"
  4. Set points to a table with three numbers (length 3)

Expected Output

Your console output should display a comma-separated summary containing:

  • The type of the nil variable
  • The boolean value
  • The numeric count
  • The string label
  • The length of the points table
  • The type of the callback function

Verify Your Answer

Verify Your Answer

Checklist

  • All values match the requested types
  • Table length is exactly 3
  • Output matches the expected ANSWER: line

Exercise 2: Truthiness Check ⭐

Premise

In Luau, only false and nil are falsy. That is a frequent source of bugs for developers coming from JavaScript, where 0 and empty strings are falsy.

Goal

By the end of this exercise, you will compute truthiness results using if in Luau.

Use Case

You are building a script that tests inputs like "opacity" and "label text." In JavaScript, an empty string or 0 would act false, but in Luau they do not. This check keeps you from disabling UI elements by mistake.

Example scenarios:

  • Validating numeric inputs like 0
  • Treating empty strings as present values

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_Truthiness
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise2 = {}

function init(self: Exercise2): boolean
-- TODO 1: Use if-statements to set these truthiness results
local zeroTruthy = false
local emptyStringTruthy = false
local nilTruthy = true

print(`ANSWER: zero={zeroTruthy},empty={emptyStringTruthy},nil={nilTruthy}`)
return true
end

function draw(self: Exercise2, renderer: Renderer)
end

return function(): Node<Exercise2>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Set zeroTruthy using if 0 then ...
  2. Set emptyStringTruthy using if "" then ...
  3. Set nilTruthy using if nil then ...

Expected Output

Your console output should display three truthiness results:

  • zero= — whether 0 is truthy in Luau
  • empty= — whether an empty string is truthy in Luau
  • nil= — whether nil is truthy in Luau

Remember: Luau's truthiness rules differ from JavaScript!


Verify Your Answer

Verify Your Answer

Checklist

  • You used if checks, not hard-coded strings
  • zeroTruthy and emptyStringTruthy are true in Luau
  • nilTruthy is false

Exercise 3: Label Builder ⭐⭐

Premise

Most Rive scripts build strings for labels, debug logs, or state banners. You need both concatenation (..) and interpolation (backticks) to format text cleanly.

Goal

By the end of this exercise, you will build the same label using concatenation and interpolation.

Use Case

You are building a scoreboard label that includes player name, level, and HP. The label is used in the UI and also logged in the Console for debugging. Creating it in two different ways shows you how to pick the most readable approach.

Example scenarios:

  • HUD labels and status text
  • Debug logs for values during animation

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_LabelBuilder
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise3 = {}

function init(self: Exercise3): boolean
local name = "Nova"
local level = 7
local hp = 85
local maxHp = 100

-- TODO 1: Build concatLabel using ..
local concatLabel = ""
-- TODO 2: Build interpLabel using backticks
local interpLabel = ""

print(`ANSWER: {concatLabel} | {interpLabel}`)
return true
end

function draw(self: Exercise3, renderer: Renderer)
end

return function(): Node<Exercise3>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Build concatLabel with .. using this format:
    • Player Nova (Lv.7) - HP 85/100
  2. Build interpLabel using backticks with the same format

Expected Output

Your console output should display two identical labels separated by " | ":

  • The first label built with concatenation (..)
  • The second label built with interpolation (backticks)

Both labels should follow the format: Player [name] (Lv.[level]) - HP [hp]/[maxHp]


Verify Your Answer

Verify Your Answer

Checklist

  • concatLabel uses .. at least twice
  • interpLabel uses backticks and {} placeholders
  • Both labels match exactly

Exercise 4: Number Formats ⭐⭐

Premise

Rive scripts use hex for colors, binary for flags, and underscores for readability. These formats are common in production scripts and should feel natural.

Goal

By the end of this exercise, you will assign values using hex, binary, and readable numeric formats and verify their decimal results.

Use Case

You are translating design specs into numbers: a hex color, a bit-flag mask, and a large time constant. Using the correct format keeps code readable and avoids mistakes when you work with artists or other engineers.

Example scenarios:

  • Converting design colors into numeric values
  • Packing multiple on/off flags into one number

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_NumberFormats
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise4 = {}

function init(self: Exercise4): boolean
-- TODO 1: Set hexValue to 0xFF
local hexValue = 0
-- TODO 2: Set binaryValue to 0b1010
local binaryValue = 0
-- TODO 3: Set readableValue to 1_000_000
local readableValue = 0
-- TODO 4: sum should be hexValue + binaryValue
local sum = 0

print(`ANSWER: hex={hexValue},bin={binaryValue},sum={sum},readable={readableValue}`)
return true
end

function draw(self: Exercise4, renderer: Renderer)
end

return function(): Node<Exercise4>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Set hexValue to 0xFF (decimal 255)
  2. Set binaryValue to 0b1010 (decimal 10)
  3. Set readableValue to 1_000_000
  4. Set sum to hexValue + binaryValue

Expected Output

Your console output should display four values:

  • hex= — the decimal equivalent of 0xFF
  • bin= — the decimal equivalent of 0b1010
  • sum= — the sum of hex and binary values
  • readable= — the number with underscores as plain decimal

All values should be displayed as plain decimal numbers.


Verify Your Answer

Verify Your Answer

Checklist

  • Hex and binary values are written using 0x / 0b formats
  • The readable number uses underscores
  • sum is 265

Exercise 5: Nil Defaults ⭐⭐

Premise

In Rive scripts, values can be nil before you assign them (especially with late bindings). Safe defaults keep your script from crashing and make your UI predictable.

Goal

By the end of this exercise, you will use or and nil checks to produce safe defaults.

Use Case

A script reads a name and lives count from external data. If those values are not present yet, the UI should still show a fallback label and a default number of lives. This is common in early initialization frames.

Example scenarios:

  • Optional ViewModel values
  • Late-bound inputs that are not set yet

Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_NilDefaults
  2. Attach and run:

    • Attach to any shape and press Play

Starter Code

--!strict

export type Exercise5 = {}

function init(self: Exercise5): boolean
local inputName: string? = nil
local inputLives: number? = nil

-- TODO 1: Use "or" to set displayName to "Guest" when inputName is nil
local displayName = ""
-- TODO 2: Use "or" to set lives to 3 when inputLives is nil
local lives = 0
-- TODO 3: hasName should be true only when inputName is not nil
local hasName = true

print(`ANSWER: name={displayName},lives={lives},hasName={hasName}`)
return true
end

function draw(self: Exercise5, renderer: Renderer)
end

return function(): Node<Exercise5>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Set displayName to inputName or "Guest"
  2. Set lives to inputLives or 3
  3. Set hasName using an explicit nil check

Expected Output

Your console output should display three values:

  • name= — the display name (either the input or the default)
  • lives= — the lives count (either the input or the default)
  • hasName= — whether the original input name was provided (not nil)

The defaults should be used since both inputs are nil.


Verify Your Answer

Verify Your Answer

Checklist

  • Defaults use or instead of hard-coded replacements later
  • hasName checks against nil explicitly
  • Output matches the expected line

Knowledge Check

Q:In Luau, what values are considered 'falsy'?
Q:What operator is used for string concatenation in Luau?
Q:What is the correct syntax for string interpolation in Luau?
Q:What does type({1, 2, 3}) return in Luau?
Q:What is the value of: local x = nil or 'default'
Q:What number format would you use to represent a color like red in Rive?

Common Mistakes

Common Data Type Errors
  1. Assuming 0 is falsy - In Luau, 0 is truthy! Use explicit == 0 checks
  2. Using + for string concatenation - Use .. instead: "Hello" .. "World"
  3. Using $ in interpolation - Use {var} not ${var} in backtick strings
  4. Forgetting nil checks - Always verify values exist before using them
  5. Confusing nil with false - nil == false is FALSE! They are different values
  6. Using [] for arrays - Use {}: {1, 2, 3} not [1, 2, 3]

Quick Reference

-- Types
nil -- No value
true, false -- Booleans
42, 3.14, 0xFF -- Numbers (all double-precision)
"hello", 'world' -- Strings
{1, 2, 3} -- Table (array)
{key = "value"} -- Table (dictionary)
function() end -- Function

-- Type checking
type(value) -- Returns "nil", "boolean", "number", "string", "table", "function"

-- String operations
"Hello" .. " World" -- Concatenation: "Hello World"
#"Hello" -- Length: 5
`Value: {var}` -- Interpolation

-- nil handling
value or default -- Default value pattern
if x ~= nil then -- Explicit nil check

-- Number formats
42 -- Decimal
0xFF -- Hexadecimal (255)
0b1010 -- Binary (10)
1_000_000 -- Readable (1000000)

Next Steps