Classes and Inheritance
Lua doesn't have classes, but WoW addons use them everywhere — via metatables, factory libraries, or just convention. wowlua-ls gives you a way to tell it about your class structures so it can provide completion, type checking, and cross-file intelligence.
Defining a class
Use @class to declare a named type with fields:
---@class AuctionEntry
---@field itemId number
---@field buyout number
---@field seller string
---@field duration number?This creates a type called AuctionEntry that you can reference anywhere:
---@param entry AuctionEntry
function displayEntry(entry)
print(entry.seller) -- completion works, type is string
print(entry.duration) -- number | nil (the ? makes it optional)
endAttaching to a variable
Usually you'll attach the class to a local that serves as the class table:
---@class AuctionEntry
---@field itemId number
---@field buyout number
---@field seller string
local AuctionEntry = {}Now AuctionEntry is both a value (the table) and a type. Methods defined on it with colon syntax are automatically part of the class:
function AuctionEntry:GetDisplayPrice()
return self.buyout -- self is typed as AuctionEntry
endFields from assignments
You don't have to declare every field up front. When you assign to self.field inside a method, the LS discovers it:
function AuctionEntry:Init(data)
self.itemId = data.itemId
self.buyout = data.buyout
self.seller = data.seller
endThe LS picks up itemId, buyout, and seller as fields. But explicit @field annotations are better because they:
- Document the type (the LS might infer
anyfrom an ambiguous RHS) - Show up in completion before you've called
Init - Enable diagnostics like
undefined-fieldandmissing-fields
Start with @field, fill in as you go
You don't need to annotate everything at once. Add @field for your core data, and let the LS discover the rest from assignments. Over time, promote discovered fields to explicit @field declarations as your type coverage improves.
Field visibility
Fields have three visibility levels:
---@class PlayerCache
---@field name string -- public (default)
---@field protected _entries table -- protected: class + subclasses
---@field private _lock boolean -- private: this class only- public — accessible from anywhere (the default)
- protected — accessible from the class and its subclasses
- private — accessible only within the class itself
Implicit protected for _ prefixes
If your project follows the _-prefix convention for internal fields, you can opt in to implicit protected visibility:
{
"inference": {
"implicit_protected_prefix": true
}
}With this enabled, data fields starting with _ are implicitly protected without needing the keyword:
---@class PlayerCache
---@field _entries table -- implicitly protected (starts with _)
---@field public _id number -- explicit public overrides the conventionThis only applies to data fields discovered at runtime (assignments, constructor fields), not to explicit @field declarations without a visibility keyword — those default to public since the author had the chance to write protected.
Methods are not affected by the _ convention. A method named _helper stays public. Use @private or @protected explicitly for methods.
Accessor visibility (@accessor)
Some addons group methods under a sub-table to signal visibility — for example, function MyClass.__p:DoSomething() where __p is a private accessor. The @accessor annotation tells the LS that methods defined through that sub-table should inherit its visibility:
---@class MyClass
---@accessor __p private
---@accessor __pt protected
local MyClass = {}
function MyClass.__p:InternalUpdate()
-- This method is private (from __p's visibility)
end
function MyClass.__pt:SharedHelper()
-- This method is protected (from __pt's visibility)
end
function MyClass:PublicMethod()
-- This method is public (no accessor)
endThe accessor name (__p, __pt) is transparent — the methods are placed directly on the class, not on a sub-table. The accessor only controls visibility. Calling obj:InternalUpdate() from outside the class triggers an access-private diagnostic.
Inheritance
Classes can extend other classes:
---@class Animal
---@field name string
---@field sound string
---@class Dog : Animal
---@field breed stringDog inherits all of Animal's fields. You can access name and sound on any Dog:
---@param dog Dog
function describe(dog)
print(dog.name) -- string (inherited from Animal)
print(dog.breed) -- string (own field)
print(dog.sound) -- string (inherited from Animal)
endDeep inheritance
Inheritance chains work to arbitrary depth:
---@class Base
---@field id number
---@class Middle : Base
---@field category string
---@class Leaf : Middle
---@field value numberLeaf has id, category, and value. The LS resolves the full chain.
Protected access in subclasses
Protected fields are accessible in subclasses:
---@class Base
---@field protected _data table
---@class Child : Base
function Child:Process()
self._data = {} -- OK, Child extends Base
endBut not from outside the hierarchy:
---@param base Base
function external(base)
base._data = {} -- warning: access-protected
endMixins and templates
WoW uses mixins extensively — Mixin() copies fields from one or more tables onto a target, and frame templates apply mixin behaviors to frames. In the type system, this is an intersection type (A & B): a value that has the fields and methods of both types.
Frame templates
When you call CreateFrame with a template, the return type is automatically an intersection of the frame type and the template mixin:
local frame = CreateFrame("Frame", nil, nil, "BackdropTemplate")
-- frame: Frame & BackdropTemplate
frame:SetPoint("CENTER") -- Frame method
frame:SetBackdrop({}) -- BackdropTemplate methodNo annotation needed — the CreateFrame stub handles this.
Annotating mixin parameters
When a function expects a frame with a specific mixin applied, use &:
---@param frame Frame & BackdropTemplate
function configureBackdrop(frame)
frame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background" })
frame:SetBackdropColor(0, 0, 0, 0.8)
endThis also works with multiple mixins:
---@param frame Frame & BackdropTemplate & UIDropDownMenuTemplate
function setupDropdown(frame) end& binds tighter than |, so Frame | Button & BackdropTemplate means Frame | (Button & BackdropTemplate).
Defining mixin types
If your addon defines its own mixins, declare them as @class types:
---@class DraggableMixin
---@field isDragging boolean
---@return nil
function DraggableMixin:StartDragging() end
---@return nil
function DraggableMixin:StopDragging() endThen reference them in intersections wherever the mixin is applied:
---@param frame Frame & DraggableMixin
function makeDraggable(frame)
frame:StartDragging() -- mixin method
frame:SetMovable(true) -- Frame method
endConstructors and missing-fields
When you construct a class instance via a table literal, the LS checks that all required fields are present:
---@class Config
---@field name string
---@field debug boolean
---@field timeout number?
---@type Config
local cfg = {
name = "MyAddon",
-- warning: missing-fields — 'debug' is required
}Optional fields (those with ? or nil in their type) don't trigger the warning.
Enum types (@enum)
Use @enum instead of @class to declare an enum type — a named table whose values are interchangeable with number:
---@enum Priority
local Priority = {
Low = 1,
Medium = 2,
High = 3,
}
---@param p Priority
function setPriority(p) end
setPriority(Priority.High) -- OK
setPriority(2) -- OK, enums accept plain numbers
setPriority("high") -- warning: type-mismatchThis is useful for any set of named numeric constants. Enum values can be passed where number is expected, and plain numbers can be passed where the enum is expected — both directions work.
WoW's built-in Enum.* types (like Enum.PowerType, Enum.UnitSex) are automatically treated as enums, so UnitPower("player", 0) doesn't produce a type-mismatch warning.
@class with metatable patterns
The most common WoW addon class pattern combines @class with metatables:
---@class Tooltip
---@field lines string[]
---@field maxWidth number
local Tooltip = {}
Tooltip.__index = Tooltip
---@return Tooltip
function Tooltip:New()
return setmetatable({
lines = {},
maxWidth = 200,
}, self)
end
function Tooltip:AddLine(text)
table.insert(self.lines, text)
end
function Tooltip:Show()
-- self.lines, self.maxWidth are typed
endThe LS understands that setmetatable({}, self) creates an instance of Tooltip through the __index chain. The @return Tooltip on New makes it explicit for callers:
local tip = Tooltip:New()
tip:AddLine("Hello") -- completion works
tip:Show() -- type checked
tip.maxWidth -- numberClass factory pattern (@defclass)
Many WoW addons use a factory function to create classes:
local Dog = MyLib:NewClass("Dog")
function Dog:Bark() endThe @defclass annotation tells the LS that a function creates classes:
---@generic T: BaseClass
---@defclass T
---@param name `T`
---@return T
function MyLib:NewClass(name) return {} endNow every call to NewClass creates a properly typed class that inherits from BaseClass. The backtick `T` means "resolve the string argument as a class name."
With parameterized parents for deep hierarchies:
---@class BaseClass<S>
---@field __super S
---@generic T: BaseClass<P>
---@generic P: BaseClass
---@defclass T : P
---@param name `T`
---@param parent? P
---@return T
function MyLib:NewClass(name, parent) return {} end
local Animal = MyLib:NewClass("Animal")
local Dog = MyLib:NewClass("Dog", Animal)
Dog.__super -- typed as Animal