A struct or a plain table
UpdatedBoth 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.
implputs 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, andDeserializewrite 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
privatefield belongs to theimpl. - A typo in a write must fail.
@sealedraises 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
typealias names it without a runtime cost. - Only the type matters.
type Point = { x: number, y: number }checks the shape, andinterfacestates 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.
