Lesson 2.3: Generics & Advanced Types
Learning Objectives
- Understand generic type parameters (
<T>) - Create reusable generic functions
- Define generic type aliases
- Use Rive's generic types (
Input<T>,Node<T>) - Apply intersection types for composition
AE/JS Syntax Comparison
If you're coming from TypeScript, Luau generics will feel very familiar:
| Concept | TypeScript | Luau (Rive) | Notes |
|---|---|---|---|
| Generic function | function foo<T>(x: T): T | function foo<T>(x: T): T | Same syntax! |
| Generic type | type Box<T> = { value: T } | type Box<T> = { value: T } | Same syntax! |
| Multiple type params | <A, B> | <A, B> | Same syntax! |
| Constraint | <T extends Foo> | Not supported | Luau lacks constraints |
| Default type | <T = string> | Not supported | Luau lacks defaults |
| Intersection | A & B | A & B | Same syntax! |
Luau generics work almost identically to TypeScript generics. The main differences:
- No generic constraints (
<T extends Foo>) - No default type parameters (
<T = string>) - The
<T>must appear before the parameter list:function foo<T>(...)notconst foo = <T>(...)
Rive Context
Rive uses generics heavily throughout its API:
Node<T>wraps your script typeInput<T>exposes values to the editorArtboard<Data.X>andInput<Artboard<Data.X>>are used for dynamic instantiation
Why does this matter? Understanding generics lets you:
- Read Rive's API signatures correctly
- Create reusable utility functions
- Build type-safe data structures
What Are Generics?
Generics let you write code that works with multiple types while maintaining type safety. Instead of writing separate functions for each type:
function getFirstString(arr: {string}): string? ... end
function getFirstNumber(arr: {number}): number? ... end
You write one generic function:
function getFirst<T>(arr: {T}): T? ... end
The <T> is a type parameter—a placeholder that gets filled in when you use the function.
JavaScript/TypeScript Equivalent:
// TypeScript - identical concept
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
// When called, T is inferred from the argument:
getFirst(["a", "b"]); // T = string
getFirst([1, 2, 3]); // T = number
Generic Function Syntax
-- Generic function
function identity<T>(value: T): T
return value
end
-- Generic type alias
type Container<T> = { value: T, count: number }
-- Multiple type parameters
function pair<A, B>(first: A, second: B): (A, B)
return first, second
end
-- Rive uses generics extensively
Input<number>
Input<Color>
Node<MyScriptType>
Artboard<Data.MyViewModel>
Practice Exercises
Exercise 1: Generic Pair ⭐
Premise
Generic aliases let you reuse one shape across multiple types. A single Pair definition can hold numbers, strings, or anything else.
By the end of this exercise, you will use a generic type alias with numbers.
Use Case
You pass a pair of values into a helper, like two timing offsets or two coordinates. Generics keep the pair definition reusable.
Example scenarios:
- Paired values in animation
- Simple tuple-like data
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_GenericPair
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise1 = {}
type Pair<T> = {
left: T,
right: T,
}
function init(self: Exercise1): boolean
-- TODO: Set left to 4 and right to 6
local pair: Pair<number> = {
left = 0,
right = 0,
}
local sum = pair.left + pair.right
print(`ANSWER: sum={sum}`)
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 left to 4 and right to 6
- Keep the Pair type generic
Expected Output
Your console output should display the sum of the two values from the generic Pair type.
Verify Your Answer
Checklist
- Generic alias is used
- Sum equals 10
- Output matches the expected line
Exercise 2: Generic Box ⭐
Premise
Generic containers keep your data structures flexible. A Box can hold any type without re-implementing the shape.
By the end of this exercise, you will use a generic box for a string value.
Use Case
You store a labeled value that might later be swapped for another type. Generics keep the container consistent.
Example scenarios:
- Generic wrappers
- Typed containers for values
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_GenericBox
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise2 = {}
type Box<T> = {
value: T,
}
function init(self: Exercise2): boolean
-- TODO: Set the boxed value to "Glow"
local boxed: Box<string> = {
value = "",
}
print(`ANSWER: value={boxed.value}`)
return true
end
function draw(self: Exercise2, renderer: Renderer)
end
return function(): Node<Exercise2>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Set the boxed value to "Glow"
- Keep the Box type generic
Expected Output
Your console output should display the boxed string value from the generic container.
Verify Your Answer
Checklist
- Generic box uses string type
- Value is correct
- Output matches the expected line
Exercise 3: Input Number ⭐
Premise
Input<T> is Rive's most common generic. It represents editor-exposed values with a concrete type.
By the end of this exercise, you will use Input<number> in a script.
Use Case
A designer sets a speed in the editor. Your script reads it and computes a distance value.
Example scenarios:
- Speed sliders
- Tunable animation rates
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_InputNumber
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise3 = {
speed: Input<number>,
}
function init(self: Exercise3): boolean
local distance = self.speed * 5
print(`ANSWER: distance={distance}`)
return true
end
function draw(self: Exercise3, renderer: Renderer)
end
return function(): Node<Exercise3>
return {
init = init,
draw = draw,
speed = 1.2,
}
end
Assignment
Complete these tasks:
- Keep speed typed as
Input<number> - Use the default speed of 1.2
Expected Output
Your console output should display the computed distance from the Input number value.
Verify Your Answer
Checklist
- Input default is numeric
- Output matches the expected line
- Input is read-only
Exercise 4: Input String ⭐
Premise
Input generics work for strings too. The type parameter tells the editor and script exactly what to expect.
By the end of this exercise, you will use an Input<string> and transform it.
Use Case
A label comes from the editor, and your script formats it for a debug overlay.
Example scenarios:
- Label inputs
- UI text formatting
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise4_InputString
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise4 = {
label: Input<string>,
}
function init(self: Exercise4): boolean
local upper = string.upper(self.label)
print(`ANSWER: label={upper}`)
return true
end
function draw(self: Exercise4, renderer: Renderer)
end
return function(): Node<Exercise4>
return {
init = init,
draw = draw,
label = "orbit",
}
end
Assignment
Complete these tasks:
- Keep label typed as
Input<string> - Use the default label "orbit"
Expected Output
Your console output should display the transformed (uppercased) string from the Input field.
Verify Your Answer
Checklist
- Input uses string type
- Uppercase formatting works
- Output matches the expected line
Exercise 5: Input Boolean ⭐⭐
Premise
Boolean inputs toggle features on and off. The generic type keeps the value strictly true or false.
By the end of this exercise, you will use an Input<boolean> to drive a toggle.
Use Case
A designer enables or disables a highlight effect. Your script uses the input to compute a numeric flag.
Example scenarios:
- Feature toggles
- Conditional effects
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise5_InputBoolean
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise5 = {
enabled: Input<boolean>,
}
function init(self: Exercise5): boolean
local flag = self.enabled and 1 or 0
print(`ANSWER: active={flag}`)
return true
end
function draw(self: Exercise5, renderer: Renderer)
end
return function(): Node<Exercise5>
return {
init = init,
draw = draw,
enabled = true,
}
end
Assignment
Complete these tasks:
- Keep enabled typed as
Input<boolean> - Use the default value true
Expected Output
Your console output should display the numeric flag derived from the boolean Input (1 when true, 0 when false).
Verify Your Answer
Checklist
- Boolean input is read correctly
- Flag equals 1 when enabled
- Output matches the expected line
Exercise 6: Generic Meter ⭐⭐
Premise
Generics let you define a reusable data shape once and supply the concrete type later. This is powerful for shared utilities.
By the end of this exercise, you will use a generic type for numeric meters.
Use Case
You measure progress across multiple values and reuse the same meter shape for each. Generics keep the structure consistent.
Example scenarios:
- Progress bars
- Resource meters
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise6_GenericMeter
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise6 = {}
type Meter<T> = {
current: T,
max: T,
}
function init(self: Exercise6): boolean
-- TODO: Set current to 25 and max to 100
local meter: Meter<number> = {
current = 0,
max = 0,
}
local percent = meter.current / meter.max
print(`ANSWER: percent={percent}`)
return true
end
function draw(self: Exercise6, renderer: Renderer)
end
return function(): Node<Exercise6>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Set current to 25 and max to 100
- Keep the meter generic and use
Meter<number>
Expected Output
Your console output should display the percent value computed from the generic Meter type (current / max).
Verify Your Answer
Checklist
- Meter uses the generic alias
- Percent equals 0.25
- Output matches the expected line
Knowledge Check
Common Mistakes
-
Forgetting the
<T>in function definition:-- WRONG: T is undefined
function foo(x: T): T
-- CORRECT: Declare T as a type parameter
function foo<T>(x: T): T -
Confusing union and intersection:
-- Union: A OR B (either type)
type StringOrNumber = string | number
-- Intersection: A AND B (all properties from both)
type NamedPosition = Named & Positioned -
Not specifying type parameters when needed:
-- WRONG: What type of elements?
local cache: Cache = createCache()
-- CORRECT: Specify the type parameter
local cache: Cache<string> = createCache() -
Trying to access properties without narrowing:
-- If you have a union, check first!
if type(value) == "number" then
-- Now you can use value as number
end
Self-Assessment Checklist
- I can write generic functions with
<T> - I can create generic type aliases
- I understand how Rive's
Input<T>andNode<T>work - I can use intersection types to combine properties
- I know when to use generics vs. specific types
Module 2 Complete!
You've finished the Type System module! Next, we'll explore Object-Oriented Programming with metatables.
→ Module 3: Object-Oriented Programming
Next Steps
- Continue to The late() Initializer
- Need a refresher? Review Quick Reference