Advanced Types
Learning Objectives
- Combine types with intersection (
&) - Create flexible values with union (
|) - Handle nullable values with optional (
?) - Define and use function types
AE/JS Syntax Comparison
| Concept | After Effects / JavaScript | Luau (Rive Scripts) |
|---|---|---|
| Union type | string | number (TS only) | string | number |
| Intersection type | A & B (TS only) | A & B |
| Optional property | name?: string | name: string? |
| Optional type | string | null | undefined | string? (same as string | nil) |
| Nullable value | null or undefined | nil |
| Function type | (x: number) => number (TS) | (number) -> number |
| Arrow function | (x) => x * 2 | function(x) return x * 2 end |
| Multiple returns | Return array/object | (number, number) -> (number, number) |
If you know TypeScript, Luau's advanced types work very similarly! The main differences are:
- Arrow syntax:
->instead of=> - Optional position:
T?(type-trailing) instead ofT \| undefined - No undefined: Luau only has
nil, not bothnullandundefined
// TypeScript - question mark BEFORE the colon
type User = { email?: string }
// Luau - question mark AFTER the type
type User = { email: string? }
This catches many TypeScript users off guard!
Rive Context
Rive scripts often need sophisticated type patterns:
- Intersection types (
&): Combine component behaviors (e.g., an object that is bothDrawableandClickable) - Union types (
|): Handle multiple possible states (e.g., animation states, response types) - Optional types (
?): Safely handle values that might not exist (e.g., optional inputs, late-bound references) - Function types: Pass callbacks for event handling, custom renderers, and behavior injection
Understanding these patterns enables you to write more expressive and type-safe Rive scripts.
Intersection Types (&)
Intersection types combine multiple types into one. A value of type A & B must have all properties from both A and B.
type Named = { name: string }
type Positioned = { x: number, y: number }
-- Must have name AND x AND y
type NamedPosition = Named & Positioned
// JavaScript/TypeScript equivalent:
type Named = { name: string }
type Positioned = { x: number, y: number }
// TypeScript intersection - identical syntax!
type NamedPosition = Named & Positioned
Union Types (|)
Union types represent "either/or" possibilities. A value of type A | B is either an A or a B.
-- Can be a number OR a string
type ID = number | string
-- Literal union for specific values
type Direction = "up" | "down" | "left" | "right"
// JavaScript/TypeScript equivalent - identical syntax:
type ID = number | string
type Direction = "up" | "down" | "left" | "right"
Optional Types (?)
Optional types are shorthand for "this type or nil". T? is equivalent to T | nil.
-- These are equivalent:
type MaybeString = string?
type MaybeString = string | nil
// JavaScript/TypeScript equivalent:
type MaybeString = string | null | undefined
// or with strictNullChecks:
type MaybeString = string | null
Practice Exercises
Exercise 1: Intersection Components ⭐⭐
Premise
Intersection types combine multiple behaviors into one. This is common when a value must satisfy several capabilities at once.
By the end of this exercise, you will build a value that matches an intersection type.
Use Case
A UI element needs a name, position, and visibility flag. Intersection types keep those requirements explicit and enforceable.
Example scenarios:
- Named drawable objects
- Components with multiple capabilities
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_Intersection
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise1 = {}
type Named = { name: string }
type Transform = { x: number, y: number }
type Drawable = { visible: boolean }
type Widget = Named & Transform & Drawable
function init(self: Exercise1): boolean
-- TODO: Fill in the widget fields
local widget: Widget = {
name = "",
x = 0,
y = 0,
visible = false,
}
local pos = widget.x + widget.y
print(`ANSWER: name={widget.name},pos={pos},visible={widget.visible}`)
return true
end
function draw(self: Exercise1, renderer: Renderer)
end
return function(): Node<Exercise1>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Set name to "Panel"
- Set x to 5 and y to 7
- Set visible to true
Expected Output
Your console output should display the intersection type fields (name, position sum, and visible flag).
Verify Your Answer
Checklist
- All intersection fields are present
- Position sum equals 12
- Output matches the expected line
Exercise 2: Union ID Format ⭐⭐
Premise
Union types allow a value to be one of several types. You must refine the type before you use it.
By the end of this exercise, you will narrow a number | string union.
Use Case
IDs sometimes arrive as numbers or strings depending on the source. You normalize them to a single display format before printing.
Example scenarios:
- Flexible IDs from JSON
- Debug labels that accept multiple types
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_UnionId
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise2 = {}
type ID = number | string
function init(self: Exercise2): boolean
local id: ID = 42
local label = ""
-- TODO: If id is a number, prefix with "#"; otherwise use it directly
print(`ANSWER: id={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:
- Use
type(id)to check if it is a number - When it is a number, build
"#<id>"
Expected Output
Your console output should display the formatted ID after narrowing the union type.
Verify Your Answer
Checklist
- Union is narrowed safely
- Label is formatted correctly
- Output matches the expected line
Exercise 3: Optional Subtitle ⭐⭐
Premise
Optional types represent values that may be nil. You must handle the nil Path before using them.
By the end of this exercise, you will handle an optional string safely.
Use Case
A subtitle may be absent on some labels. You need a fallback to avoid nil values in text output.
Example scenarios:
- Optional text labels
- Optional metadata
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_OptionalSubtitle
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise3 = {}
type Label = {
text: string,
subtitle: string?,
}
function init(self: Exercise3): boolean
local label: Label = {
text = "Signal",
subtitle = nil,
}
-- TODO: Use "None" when subtitle is nil
local subtitle = ""
print(`ANSWER: subtitle={subtitle}`)
return true
end
function draw(self: Exercise3, renderer: Renderer)
end
return function(): Node<Exercise3>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Check for a nil subtitle
- Use "None" as the fallback
Expected Output
Your console output should display the subtitle value or the fallback when the optional is nil.
Verify Your Answer
Checklist
- Optional value is handled
- Subtitle is never nil
- Output matches the expected line
Exercise 4: Function Type Alias ⭐⭐
Premise
Function types define callable shapes. This lets you enforce that callbacks accept and return the right values.
By the end of this exercise, you will use a function type alias for an easing helper.
Use Case
You pass easing functions into animation helpers. Strong types ensure the function signature is consistent.
Example scenarios:
- Easing utilities
- Animation callbacks
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise4_FunctionAlias
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise4 = {}
type EaseFn = (t: number) -> number
function init(self: Exercise4): boolean
local easeIn: EaseFn = function(t: number): number
-- TODO: Return t * t
return 0
end
local eased = easeIn(0.5)
print(`ANSWER: eased={eased}`)
return true
end
function draw(self: Exercise4, renderer: Renderer)
end
return function(): Node<Exercise4>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Return t * t
- Keep the function typed as EaseFn
Expected Output
Your console output should display the eased value from the function type alias.
Verify Your Answer
Checklist
- Function signature matches EaseFn
- Output equals 0.25
- Output matches the expected line
Exercise 5: Literal Direction Union ⭐⭐
Premise
Literal unions restrict values to a safe set. This prevents invalid direction strings from breaking logic.
By the end of this exercise, you will use a literal union to map directions.
Use Case
You control a small movement direction for a procedural effect. The value must be one of a few known directions.
Example scenarios:
- Directional motion
- State-driven movement
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise5_LiteralDirection
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise5 = {}
type Direction = "up" | "down" | "left" | "right"
function init(self: Exercise5): boolean
local direction: Direction = "left"
local dx = 0
local dy = 0
-- TODO: Map the direction to dx/dy
print(`ANSWER: dx={dx},dy={dy}`)
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 dx to -1 when direction is "left"
- Set dy to 0 for left
Expected Output
Your console output should display the dx and dy values mapped from the literal direction union.
Verify Your Answer
Checklist
- Direction is one of the literals
- dx/dy mapping is correct
- Output matches the expected line
Exercise 6: Multiple Returns ⭐⭐
Premise
Luau supports multiple return values, and types can describe them. This is useful for compact helpers that return pairs.
By the end of this exercise, you will implement a function with typed multiple returns.
Use Case
A helper returns min and max values for a range. You then compute a span from those two values.
Example scenarios:
- Range helpers
- Returning paired values
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise6_MultiReturn
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise6 = {}
local function bounds(): (number, number)
-- TODO: Return 2 and 10
return 0, 0
end
function init(self: Exercise6): boolean
local minValue, maxValue = bounds()
local range = maxValue - minValue
print(`ANSWER: range={range}`)
return true
end
function draw(self: Exercise6, renderer: Renderer)
end
return function(): Node<Exercise6>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Return 2 and 10 from bounds
- Keep the function return types as numbers
Expected Output
Your console output should display the range computed from the multiple return values.
Verify Your Answer
Checklist
- Function returns two numbers
- Range equals 8
- Output matches the expected line
Knowledge Check
Common Mistakes
1. Accessing Union Properties Without Narrowing
-- WRONG: Can't access .speed without narrowing
local function process(state: IdleState | WalkingState)
print(state.speed) -- ERROR: IdleState has no 'speed'
end
-- CORRECT: Check kind first
local function process(state: IdleState | WalkingState)
if state.kind == "walking" then
print(state.speed) -- OK: Luau knows it's WalkingState
end
end
2. Forgetting to Handle nil
-- WRONG: Accessing optional without check
local function getName(profile: {name: string?}): string
return profile.name -- ERROR: might be nil
end
-- CORRECT: Handle nil case
local function getName(profile: {name: string?}): string
return profile.name or "Anonymous"
end
3. Confusing | and &
-- Union (|): Can be A OR B - use for alternatives
type Result = Success | Failure -- One or the other
-- Intersection (&): Must be A AND B - use for composition
type Character = Named & Positioned -- Has both
4. Missing Return Type on Function Types
-- WRONG: Incomplete function type
type BadFunc = (number) -- What does it return?
-- CORRECT: Include return type
type GoodFunc = (number) -> number
type VoidFunc = (number) -> () -- Returns nothing
5. Using Intersection When Union Is Needed
-- WRONG: Object can't be BOTH string AND number
type ID = string & number -- Impossible!
-- CORRECT: Object is string OR number
type ID = string | number -- Either one
Self-Assessment Checklist
- I can combine types with intersection (
&) - I can create flexible values with union (
|) - I know when to use literal unions for constrained values
- I can safely handle optional (
?) values - I can define function types and use callbacks
- I understand the difference between
|and&
Next Steps
- Continue to 2.3 Generics & Advanced Types
- Need a refresher? Review Quick Reference