Implementing OOP in Luau — Complete Guide to Metatables, Class Patterns & Inheritance [2026]
Complete guide to implementing OOP in Luau. Covers metatable-based class patterns with __index, type-safe class definitions, inheritance, metamethods, constructor patterns, private field emulation, and ModuleScript organization with code examples.
OOP in Luau is achieved using metatables, not class syntax
Luau has no class keyword. By combining tables with the `__index` metamethod, you can fully replicate object-oriented programming. This guide covers everything from basic patterns to inheritance and type-safe class design.
Why Luau Has No Class Syntax
Luau is based on Lua 5.1 and intentionally omits class syntax to preserve the language's simplicity and extensibility philosophy. However, Luau's metatable system is powerful enough to implement all OOP concepts. Large-scale Roblox games rely on this approach as standard practice.
Basic Class Pattern: Creating an Animal Class
Here is the most widely used class pattern in Luau:
local Animal = {}
Animal.__index = Animal
function Animal.new(name: string, sound: string)
local self = setmetatable({}, Animal)
self.name = name
self.sound = sound
return self
end
function Animal:speak()
print(self.name .. " says " .. self.sound)
end
function Animal:getName(): string
return self.name
end
return AnimalThe key line is `Animal.__index = Animal`. When an instance is created with `setmetatable({}, Animal)`, method lookups fall back to the Animal table automatically.
Type-Safe Class Definitions Using Luau's Type System
Leveraging Luau's type system enables IDE completion and static analysis:
local Animal = {}
Animal.__index = Animal
export type Animal = typeof(setmetatable(
{} :: {
name: string,
sound: string,
},
Animal
))
function Animal.new(name: string, sound: string): Animal
local self = setmetatable({} :: { name: string, sound: string }, Animal)
self.name = name
self.sound = sound
return self
endUsing `export type` allows other ModuleScripts to reference this type directly.
Inheritance Pattern: Dog Extends Animal
Inheritance in Luau works by chaining the parent class into the `__index` of the child:
local Animal = require(script.Parent.Animal)
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
export type Dog = typeof(setmetatable(
{} :: { breed: string },
Dog
))
function Dog.new(name: string, breed: string): Dog
local self = setmetatable(Animal.new(name, "Woof") :: any, Dog)
self.breed = breed
return self
end
function Dog:fetch(item: string)
print(self.name .. " fetches the " .. item)
end
return Dog`setmetatable({}, { __index = Animal })` makes the Dog table itself inherit Animal's methods. Instances check Dog first, then fall back to Animal.
Metamethods Reference
| Metamethod | Purpose | Example Use Case |
|---|---|---|
| `__index` | Property/method lookup fallback | Foundation of class inheritance |
| `__newindex` | Control assignment to undefined keys | Read-only properties |
| `__tostring` | Customize `tostring()` output | Debug display |
| `__add` | Overload the `+` operator | Vector addition |
| `__eq` | Custom equality comparison | Value-based equality |
| `__len` | Overload the `#` operator | Custom collection length |
| `__call` | Call a table like a function | Functor pattern |
| `__concat` | Overload the `..` operator | Custom string concatenation |
Constructor Patterns: .new() vs :init()
| Pattern | Syntax | Pros | Cons |
|---|---|---|---|
| `.new()` | `Animal.new(name)` | Community standard, strong type inference | self must be explicit |
| `:new()` | `Animal:new(name)` | auto-passes self | class itself becomes self, causing confusion |
| `:init()` | Separate init method | Reusable initialization logic | Two-step creation required |
The Roblox community has standardized on `.new()`. Official Roblox libraries consistently use this pattern.
Simulating Private Fields
Luau has no access modifiers. Two common approaches exist: Underscore Convention (Conventional Private)
function Animal.new(name: string)
local self = setmetatable({}, Animal)
self._name = name -- naming convention signals internal use
return self
endClosure Pattern (True Privacy)
local function createAnimal(name: string)
local _name = name -- inaccessible from outside
return {
getName = function() return _name end,
setName = function(n) _name = n end,
}
endThe closure pattern provides true encapsulation but cannot leverage metatable-based inheritance.
Organizing Classes with ModuleScripts
Best practice is to isolate each class as a ModuleScript:
ServerScriptService
└── Classes
├── Animal.luau (ModuleScript)
├── Dog.luau (ModuleScript)
└── Cat.luau (ModuleScript)Each ModuleScript ends with `return Animal`. The consumer loads it with `local Animal = require(script.Parent.Classes.Animal)`. Avoid circular dependencies by keeping dependency direction strictly one-way.
Community Libraries: Classe and ClasseV2
When manual OOP setup feels repetitive, community libraries can help:
| Library | Features | Wally Package |
|---|---|---|
| Classe | Simple OOP abstraction, lightweight | `classe/classe` |
| ClasseV2 | Type-safe, strict mode support | `classe/classev2` |
| OOP-Utils | Multiple inheritance support | Community distributed |
For small to medium projects, mastering the standard metatable pattern is more valuable long-term.
Frequently Asked Questions
Q1. Why won't class syntax ever be added to Luau? A. Lua's design philosophy prioritizes simplicity and extensibility. Since metatables achieve equivalent functionality, a language-level class construct is considered unnecessary. Q2. How does this compare to TypeScript classes? A. TypeScript provides compile-time type checking and true prototype inheritance. Luau's metatables are runtime dynamic dispatch, supplemented by Luau's type system for static analysis. Q3. Can __index be a function instead of a table? A. Yes. `__index = function(self, key) ... end` allows custom control over property access, which is useful for dynamic proxy patterns. Q4. Is there a performance impact from metatables? A. Metatable lookups have slight overhead, but at Roblox game scales this is rarely a concern. Be mindful when creating large numbers of instances in hot paths. Q5. Can multiple inheritance be implemented? A. Not directly, but a mixin pattern — where `__index` is a function that searches multiple tables — achieves it in practice. Q6. Does this work with --!strict mode? A. Yes. Strict mode requires type annotations, but the `export type` and `typeof(setmetatable(...))` pattern handles this fully. Q7. Is there an official Roblox-recommended OOP pattern? A. Yes. The official Roblox documentation presents the `.new()` constructor and `__index` metatable pattern as the standard approach.
Professional Roblox Development Support by Oflight
Need expert support for Roblox game development using Luau? Oflight provides professional assistance from OOP architecture design to module organization and performance optimization. Visit our Software Development Service to learn more.
Feel free to contact us
Contact Us