Skip to main content

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:

ConceptTypeScriptLuau (Rive)Notes
Generic functionfunction foo<T>(x: T): Tfunction foo<T>(x: T): TSame syntax!
Generic typetype 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 supportedLuau lacks constraints
Default type<T = string>Not supportedLuau lacks defaults
IntersectionA & BA & BSame syntax!
If You Know TypeScript

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>(...) not const foo = <T>(...)

Rive Context​

Rive uses generics heavily throughout its API:

  • Node<T> wraps your script type
  • Input<T> exposes values to the editor
  • Artboard<Data.X> and Input<Artboard<Data.X>> are used for dynamic instantiation

Why does this matter? Understanding generics lets you:

  1. Read Rive's API signatures correctly
  2. Create reusable utility functions
  3. 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.

Goal

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:​

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_GenericPair
  2. Add and run:

    • Add the script to the artboard, position or nest the script node, then press Play

Starter Code​

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:

  1. Set left to 4 and right to 6
  2. 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​

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.

Goal

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:​

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_GenericBox
  2. Add and run:

    • Add the script to the artboard, position or nest the script node, then press Play

Starter Code​

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:

  1. Set the boxed value to "Glow"
  2. Keep the Box type generic

Expected Output​

Your console output should display the boxed string value from the generic container.


Verify Your Answer​

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.

Goal

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:​

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_InputNumber
  2. Add and run:

    • Add the script to the artboard, position or nest the script node, then press Play

Starter Code​

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:

  1. Keep speed typed as Input<number>
  2. 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​

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.

Goal

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:​

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_InputString
  2. Add and run:

    • Add the script to the artboard, position or nest the script node, then press Play

Starter Code​

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:

  1. Keep label typed as Input<string>
  2. Use the default label "orbit"

Expected Output​

Your console output should display the transformed (uppercased) string from the Input field.


Verify Your Answer​

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.

Goal

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:​

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_InputBoolean
  2. Add and run:

    • Add the script to the artboard, position or nest the script node, then press Play

Starter Code​

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:

  1. Keep enabled typed as Input<boolean>
  2. 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​

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.

Goal

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:​

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise6_GenericMeter
  2. Add and run:

    • Add the script to the artboard, position or nest the script node, then press Play

Starter Code​

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:

  1. Set current to 25 and max to 100
  2. 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​

Verify Your Answer

Checklist​

  • Meter uses the generic alias
  • Percent equals 0.25
  • Output matches the expected line

Knowledge Check​

Q:What does <T> mean in a function definition?
Q:How is Input<number> different from just number?
Q:What's the difference between union (|) and intersection (&) types?
Q:Why might you use Entity<T> instead of separate Player, Enemy, Item types?

Common Mistakes​

Avoid These Errors
  1. 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
  2. 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
  3. 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()
  4. 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> and Node<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​