lloyAlloy on GitHub
A struct or a plain table
Docs

A struct or a plain table

Updated

Both hold fields, and both are a Luau table at run time. A struct adds a name, a checked shape, and methods; a plain table adds nothing and asks nothing.

import { Serialize, Deserialize } from "@alloy/std/serde"

-- A struct: a named shape with behavior.
@derive(Eq, Clone, Serialize, Deserialize)
struct Loadout
    weapon: string
    level: number = 1
end

impl Loadout
    function upgrade(self) self.level += 1 end
end

local kit = new Loadout { weapon = "sword" }

-- A plain table: data that passes through.
local settings = { volume = 0.5, music = true }

Use a struct when:

  • The shape is fixed and has a name the code talks about: a player's save, a loadout, a projectile.
  • The value has behavior. impl puts methods on it, and a trait says what it can do.
  • You want the compiler to check construction. new Loadout { } reports a field left out and a field the struct lacks; a table literal reports neither.
  • You want derived code: Eq, Ord, Clone, Debug, Default, Serialize, and Deserialize write it for you.
  • The value crosses a remote. A struct packs into a buffer field by field, and it arrives with its metatable back.
  • Some fields are the owner's alone. A private field belongs to the impl.
  • A typo in a write must fail. @sealed raises on a key the struct does not declare.

Use a plain table when:

  • The data is one-off: the options of one call, a return with two values in it, a literal passed straight to a library.
  • The keys are data, not names: a map from a player's id to a score. HashMap, or a { [K]: V } table, fits.
  • The shape belongs to someone else: a Luau module, a Roblox API, JSON from HttpService. A type alias names it without a runtime cost.
  • Only the type matters. type Point = { x: number, y: number } checks the shape, and interface states a contract many shapes meet.

What a struct costs. A field read is a plain table read, as fast as on any table. A method call takes one metatable lookup, the same as every Luau class. Construction runs the defaults the struct declares. @alias and @sealed add a metamethod on each read or write of an absent key, so leave them off a struct in a hot loop.

Moving between them. @derive(Serialize, Deserialize) turns a struct into a plain table and back: to_table() for a DataStore or a log, and from_table(t) for what comes back. A plain table that grows methods and a fixed shape is a struct waiting to be declared.