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

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

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

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

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

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

  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