Strict Mode Deep Dive
Learning Objectives
- Understand the three type checking modes in Luau
- Know that Rive scripts run in strict mode by default
- Recognize and fix common type errors caught by strict mode
- Use strict mode effectively in your development workflow
AE/JS Syntax Comparison
If you're coming from After Effects expressions or JavaScript, understanding type checking modes is crucial:
| Concept | JavaScript/AE | Luau (Rive) | Notes |
|---|---|---|---|
| Type checking | None (runtime only) | --!strict (compile time) | Luau catches errors BEFORE running |
| Untyped mode | Default | --!nocheck | Both skip type checking |
| Strict mode | N/A | --!strict | Recommended for Rive |
| TypeScript | tsc --strict | --!strict | Similar concept |
| Empty array | [] works | {} needs type | local t: {string} = {} |
| Missing property | Runtime error | Compile error | Strict catches early |
JavaScript: Bugs crash at runtime, sometimes only in edge cases
// JavaScript - crashes when health is undefined
let result = player.health - damage; // No error until runtime!
Luau Strict: Bugs caught immediately during development
-- Luau --!strict - error shown in editor BEFORE running
local result = player.health - damage -- Error: health might be nil
This is WHY strict mode is strongly recommended—your animations must work reliably everywhere, and there's no debugger when they run on users' devices.
Rive Context
Rive scripts run in strict mode by default for type safety. This means all the benefits below are enabled automatically:
- Type safety at compile time: Errors are caught before your animation runs
- Better editor integration: Autocomplete knows exact types
- Reliable runtime behavior: No surprise
nilvalues or type mismatches - Cleaner code: Forces you to think about data flow
Why does this matter? When your animation ships to users, there's no debugger. Strict mode ensures your animation works correctly because all type errors were caught during development.
When you write a Node Script, the Rive compiler expects proper type annotations for your export type, function parameters, and return values. Missing or incorrect types will prevent your script from compiling.
The Three Type Checking Modes
Luau supports three modes, controlled by a directive at the top of your file:
--!strict -- Full type checking (RECOMMENDED for Rive)
--!nonstrict -- Partial checking, allows 'any' inference
--!nocheck -- No type checking
Mode Comparison
| Feature | --!nocheck | --!nonstrict | --!strict |
|---|---|---|---|
| Type inference | No | Partial | Full |
| Errors on type mismatch | No | Some | Yes |
| Requires annotations | No | Sometimes | Yes (for functions) |
| Used in Rive | No | No | Yes |
Practice Exercises
Exercise 1: Parse Text Safely ⭐
Premise
Strict mode refuses to mix text and numbers in math. When data arrives as strings, you must convert it explicitly.
By the end of this exercise, you will convert text to numbers before arithmetic.
Use Case
A UI field or designer Input provides a string value, but you need numeric math for animation. Parsing explicitly keeps strict mode happy and avoids hidden coercion bugs.
Example scenarios:
- Parsing user-entered values
- Converting serialized data
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_ParseText
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise1 = {}
function init(self: Exercise1): boolean
local maxHealth = 100
local damageText: string = "25"
local damage = 0
-- TODO: Convert damageText into a number and store in damage
local remaining = maxHealth - damage
print(`ANSWER: remaining={remaining}`)
return true
end
function draw(self: Exercise1, renderer: Renderer)
end
return function(): Node<Exercise1>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Use
tonumberto convertdamageText - If conversion fails, keep damage as 0
Expected Output
Your console output should display the remaining health after converting the string damage to a number and subtracting it.
Verify Your Answer
Checklist
- No string math remains
- damage is a number
- Output matches the expected line
Exercise 2: Optional Label ⭐
Premise
Optional values are common in strict mode. You must check for nil before using them.
By the end of this exercise, you will handle an optional string safely.
Use Case
A label may or may not be provided by a designer. You need a safe fallback so the UI always has text.
Example scenarios:
- Optional text overrides
- Missing configuration values
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_OptionalLabel
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise2 = {}
function init(self: Exercise2): boolean
local hint: string? = nil
local label = ""
-- TODO: Use hint when present, otherwise use "Default"
print(`ANSWER: label={label}`)
return true
end
function draw(self: Exercise2, renderer: Renderer)
end
return function(): Node<Exercise2>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Check if hint is nil
- Use "Default" when hint is missing
Expected Output
Your console output should display the label value — either the hint (if provided) or the fallback default string.
Verify Your Answer
Checklist
- Optional value is checked before use
- label is always a string
- Output matches the expected line
Exercise 3: Return Paths ⭐
Premise
Strict mode requires every code Path to return a value that matches the function signature. Clear boolean helpers avoid hidden nil returns.
By the end of this exercise, you will return a boolean on all paths.
Use Case
You are flagging a critical state for a health indicator. The helper must always return true or false.
Example scenarios:
- Critical health checks
- Toggle guards
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_ReturnPaths
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise3 = {}
local function isCritical(hp: number): boolean
-- TODO: Return true when hp <= 10, otherwise false
return false
end
function init(self: Exercise3): boolean
local critical = isCritical(8)
print(`ANSWER: critical={critical}`)
return true
end
function draw(self: Exercise3, renderer: Renderer)
end
return function(): Node<Exercise3>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Return true when hp is 10 or less
- Return false otherwise
Expected Output
Your console output should display whether the health value is at or below the critical threshold (returns true or false).
Verify Your Answer
Checklist
- Function returns on all paths
- Output matches the expected line
- No nil returns
Exercise 4: Literal Union Mode ⭐
Premise
Literal unions limit a variable to known values. This keeps State Machine values predictable and typo-proof.
By the end of this exercise, you will use a literal union type.
Use Case
You are switching between animation modes. Using a literal union prevents invalid strings from slipping in.
Example scenarios:
- State machine modes
- Named animation states
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise4_LiteralUnion
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise4 = {}
type Mode = "idle" | "run" | "hit"
function init(self: Exercise4): boolean
local mode: Mode = "idle"
-- TODO: Set mode to "run"
print(`ANSWER: mode={mode}`)
return true
end
function draw(self: Exercise4, renderer: Renderer)
end
return function(): Node<Exercise4>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Set the mode to "run"
- Keep the mode typed as
Mode
Expected Output
Your console output should display the current mode value from the literal union type.
Verify Your Answer
Checklist
- Mode uses the literal union type
- Value is one of the allowed literals
- Output matches the expected line
Exercise 5: Table Shape Enforcement ⭐⭐
Premise
Strict mode enforces table shapes. If a table is typed, every field must match its definition.
By the end of this exercise, you will work with a typed table and valid fields.
Use Case
You are storing stats for a UI meter. Correct field types ensure your calculations never receive unexpected values.
Example scenarios:
- Typed stats tables
- Parameter packs for helpers
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise5_TableShape
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise5 = {}
type Stats = {
hp: number,
mp: number,
}
function init(self: Exercise5): boolean
-- TODO: Set hp to 80 and mp to 20
local stats: Stats = {
hp = 0,
mp = 0,
}
local ratio = stats.hp / stats.mp
print(`ANSWER: ratio={ratio}`)
return true
end
function draw(self: Exercise5, renderer: Renderer)
end
return function(): Node<Exercise5>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Set hp to 80 and mp to 20
- Keep the table typed as
Stats
Expected Output
Your console output should display the ratio computed from the typed stats table (hp divided by mp).
Verify Your Answer
Checklist
- Table fields match the Stats type
- Ratio equals 4
- Output matches the expected line
Exercise 6: Input Safety ⭐⭐
Premise
Input<T> values are intended to be read-only. While not runtime-enforced, you should derive writable state from inputs rather than reassigning them.
By the end of this exercise, you will compute a derived value from an input.
Use Case
A designer sets a speed input. Your script computes an adjusted value used for animation timing.
Example scenarios:
- Speed multipliers
- Difficulty scaling
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise6_InputSafety
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise6 = {
speed: Input<number>,
adjusted: number,
}
function init(self: Exercise6): boolean
-- TODO: Compute adjusted as speed * 1.2
self.adjusted = 0
print(`ANSWER: adjusted={self.adjusted}`)
return true
end
function draw(self: Exercise6, renderer: Renderer)
end
return function(): Node<Exercise6>
return {
init = init,
draw = draw,
speed = 2.5,
adjusted = late(),
}
end
Assignment
Complete these tasks:
- Read speed from the input
- Store speed * 1.2 in adjusted
Expected Output
Your console output should display the adjusted value computed by multiplying the speed input by the factor.
Verify Your Answer
Checklist
- Input is read-only
- adjusted is computed in init
- Output matches the expected line
Knowledge Check
Common Mistakes
-
Strict mode is default: Rive scripts run in strict mode by default. You can add
--!strictexplicitly for clarity, but it's not required.-- Strict mode is already enabled by default in Rive
-- Adding --!strict is optional but makes intent clear
export type MyScript = {} -
Empty tables without types:
local t: {string} = {}notlocal t = {} -
Plain tables for Rive types: Use
Vector.xy()not{x = 100, y = 50} -
Not handling optionals: Always check
string?before using asstring-- WRONG
print(user.email:upper())
-- CORRECT
if user.email then
print(user.email:upper())
end -
Missing function return types: Top-level functions need explicit return type annotations
-- WRONG
local function add(a: number, b: number)
return a + b
end
-- CORRECT
local function add(a: number, b: number): number
return a + b
end
Self-Assessment Checklist
- I understand that Rive scripts run in strict mode by default
- I can fix "cannot infer type of empty table" errors
- I know the difference between strict, nonstrict, and nocheck modes
- I can properly annotate function signatures
- I understand how strict mode narrows types through control flow
- I use Rive's constructors (Vector.xy, Color.rgba) correctly
Next Steps
- Continue to 2.2 Custom Types & Unions
- Need a refresher? Review Quick Reference