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
Library and namespace tables
The rules above are written for class instances accessed through self inside methods. They also work for the common addon pattern where a table is itself the class — a library or an addon namespace:
-- LibFoo.lua (declares the class)
---@class LibFoo
---@field private callbackMap table<any, fun()>
LibFoo = LibFoo or {}
LibFoo.callbackMap = LibFoo.callbackMap or {} -- ok: initialized in its own file
for _, cb in pairs(LibFoo.callbackMap) do end -- ok: used in its own fileInside the file that declares ---@class LibFoo, the class's own table can freely read and write its private/protected fields at file scope — including the reload-safe LibFoo.x = LibFoo.x or {} idiom. This applies when the table is named after the class it declares — a namespace global (LibFoo for ---@class LibFoo) or a local module table (local Foo = {} for ---@class Foo). In any other file those fields are hidden from autocomplete and hover, and touching them raises access-private / access-protected:
-- SomeConsumer.lua
LibFoo.callbackMap[1] = fn -- access-private: 'callbackMap' is privateThe leniency is only for the class's own table accessed directly. A handle typed as the class (local h = {} ---@type LibFoo, or a global under a different name) counts as a consumer even in the declaring file, and a different class reached through a field (LibFoo.inner.secret) is judged as itself — both still warn. Reach a private field through the class's own table (LibFoo.callbackMap) or a method.
Field visibility can be declared either in the @class block (@field private) or inline on the assignment, next to the code — handy when a field is initialized where it lives rather than up top:
---@class LibFoo
LibFoo = LibFoo or {}
--- @private
--- @type table<any, fun()>
LibFoo.callbackMap = LibFoo.callbackMap or {}Both forms produce the same visibility; use whichever reads better.
Implicit protected for _ prefixes
If your project follows the _-prefix convention for internal fields, you can opt in to implicit protected visibility:
{
"inference": {
"implicitProtectedPrefix": 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)
endMultiple parents
A class can inherit from multiple parents. Use commas or &; both are equivalent:
---@class CellMixin
---@field cellWidth number
---@class TooltipMixin
---@field tooltipText string
---@class MyCellTemplate : CellMixin, TooltipMixin
---@field label stringOr with &, which reads naturally when the parents are mixins:
---@class MyCellTemplate : CellMixin & TooltipMixin
---@field label stringMyCellTemplate inherits cellWidth from CellMixin and tooltipText from TooltipMixin:
---@type MyCellTemplate
local cell = {}
cell.cellWidth -- number (from CellMixin)
cell.tooltipText -- string (from TooltipMixin)
cell.label -- string (own field)You can mix the two syntaxes and combine with table<K,V>:
---@class TaggedMixin : CellMixin & TooltipMixin, table<string, number>Dictionary classes (table<K,V> parent)
A class can inherit from table<K,V> to combine a named class type with dictionary key/value types. This gives pairs() loops typed keys and values:
---@class ColorMap : table<string, string>
---@field default string
---@type ColorMap
local colors = { Red = "#FF0000", Blue = "#0000FF" }
for name, hex in pairs(colors) do
-- name: string, hex: string
end
local d = colors.default -- string (named field)You can combine this with regular class inheritance:
---@class Base
---@field id number
---@class TaggedScores : Base, table<string, number>TaggedScores inherits id from Base and has typed string keys / number values.
Deep 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.
Mixin(), CreateFromMixins(), CreateAndInitFromMixin()
WoW's mixin functions are fully typed. They use variadic generics to support any number of mixins:
local frame = Mixin(CreateFrame("Frame"), DraggableMixin, TooltipMixin, ScrollableMixin)
-- frame: Frame & DraggableMixin & TooltipMixin & ScrollableMixinMixin() also supports bare calls via @narrows-arg: the first argument's type is updated in-place:
---@type Frame
local frame = CreateFrame("Frame")
Mixin(frame, DraggableMixin)
-- frame is now Frame & DraggableMixin
frame:StartDragging() -- worksCreateFromMixins() creates a new object from mixins:
local obj = CreateFromMixins(DraggableMixin, TooltipMixin)
-- obj: DraggableMixin & TooltipMixinAnnotating 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
endPartial classes
The (partial) and (exact) modifiers are accepted by the parser for compatibility, but are currently ignored: they have no effect on diagnostics. This means code using @class (partial) won't cause parse errors, but the class is still treated as exact.
---@class (partial) AddonState -- parsed, but treated the same as @class AddonState
---@field version numberConstructors 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 bidirectionally compatible with their value type (number or string):
---@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-mismatchString-valued enums work the same way: values are interchangeable with string:
---@enum Status
local Status = {
Active = "active",
Inactive = "inactive",
Pending = "pending",
}
---@param s Status
function setStatus(s) end
setStatus(Status.Active) -- OK
setStatus("custom") -- OK, string enums accept plain strings
setStatus(42) -- warning: type-mismatchThe enum's value type is inferred automatically from the field values. Values may be literals or references to constants/variables — Low = LOW_PRIORITY is inferred just like Low = 1. All values must be the same type; mixing numbers and strings in the same enum produces a mixed-enum-values warning.
Key-based enums (@enum (key))
By default, @enum creates a type from the table's values. Use @enum (key) to create an enum from the table's keys instead, useful when a table's keys represent a fixed set of valid string identifiers:
---@enum (key) Settings
local DEFAULTS = { showTooltip = true, maxRetries = 5, prefix = "My" }
---@param setting Settings
---@return any
function getSetting(setting)
return DEFAULTS[setting]
end
getSetting("showTooltip") -- OK
getSetting("unknown") -- OK (string-compatible, like all string enums)
getSetting(42) -- warning: type-mismatchKey enums are always string enums (since Lua table constructor keys are identifiers). The mixed-enum-values diagnostic does not apply to key enums; their values can be any type.
WoW's built-in Enum.* types (like Enum.PowerType, Enum.UnitSex) are automatically treated as number 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 AnimalEmbedding libraries via varargs
Some factories mix additional classes into the object they create, named by extra string arguments — the Ace3 idiom NewAddon(name, "AceEvent-3.0", …), where each trailing library name is embedded (its methods become available on the addon).
Give the ... vararg a backtick type to model this: each string-literal argument past the class-name position is added as a parent (mixin) of the created class.
---@generic T: AceAddon
---@defclass T : AceAddon
---@param name `T`
---@param ... `T` @ library names to embed
---@return T
function AceAddon:NewAddon(name, ...) end
local Addon = AceAddon:NewAddon("MyAddon", "AceEvent-3.0", "AceConsole-3.0")
Addon:RegisterEvent("PLAYER_LOGIN", "OnLogin") -- from AceEvent-3.0
Addon:Print("hello") -- from AceConsole-3.0MyAddon is created inheriting AceAddon and AceEvent-3.0 and AceConsole-3.0, so their instance methods resolve on the addon object (and on self inside its methods) without hand-writing ---@class MyAddon : AceEvent-3.0, …. This is built into the bundled Ace3 typings for NewAddon and NewModule.
