Skip to main content

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

ConceptAfter Effects / JavaScriptLuau (Rive Scripts)
Union typestring | number (TS only)string | number
Intersection typeA & B (TS only)A & B
Optional propertyname?: stringname: string?
Optional typestring | null | undefinedstring? (same as string | nil)
Nullable valuenull or undefinednil
Function type(x: number) => number (TS)(number) -> number
Arrow function(x) => x * 2function(x) return x * 2 end
Multiple returnsReturn array/object(number, number) -> (number, number)
TypeScript Users

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 of T \| undefined
  • No undefined: Luau only has nil, not both null and undefined
Critical Difference: Optional Property Syntax
// 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:

  1. Intersection types (&): Combine component behaviors (e.g., an object that is both Drawable and Clickable)
  2. Union types (|): Handle multiple possible states (e.g., animation states, response types)
  3. Optional types (?): Safely handle values that might not exist (e.g., optional inputs, late-bound references)
  4. 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_Intersection
  2. 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:

  1. Set name to "Panel"
  2. Set x to 5 and y to 7
  3. Set visible to true

Expected Output

Your console output should display the intersection type fields (name, position sum, and visible flag).


Verify Your Answer

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_UnionId
  2. 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:

  1. Use type(id) to check if it is a number
  2. 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

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_OptionalSubtitle
  2. 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:

  1. Check for a nil subtitle
  2. 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

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_FunctionAlias
  2. 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:

  1. Return t * t
  2. Keep the function typed as EaseFn

Expected Output

Your console output should display the eased value from the function type alias.


Verify Your Answer

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_LiteralDirection
  2. 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:

  1. Set dx to -1 when direction is "left"
  2. 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

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise6_MultiReturn
  2. 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:

  1. Return 2 and 10 from bounds
  2. 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

Verify Your Answer

Checklist

  • Function returns two numbers
  • Range equals 8
  • Output matches the expected line

Knowledge Check

Q:What does the & operator do in type definitions?
Q:What is the difference between string? and string | nil?
Q:What is the correct function type syntax for a function that takes two numbers and returns a string?
Q:When should you use a union type vs. an intersection type?

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