Builder Pattern
Some WoW addons define data schemas through method-chaining builders — each call adds a typed field, and a final call returns the accumulated type. wowlua-ls tracks these chains and gives you full completion and type checking on the result.
Basic builder
Three annotations power the builder pattern:
@builds-field <param_idx> <type>— this method adds a field whose name is the string at parameter positionparam_idx@return self— the method returns the same receiver (for chaining)@return built— returns the accumulated type with all added fields
---@class Schema
local Schema = {}
---@param name string
---@builds-field 1 string
---@return self
function Schema:AddString(name) return self end
---@param name string
---@builds-field 1 number?
---@return self
function Schema:AddNumber(name) return self end
---@return built
function Schema:Build() return {} endNow chain them:
local inst = Schema:AddString("label"):AddNumber("count"):Build()
inst.label -- string
inst.count -- number?Each @builds-field call adds a field. @return self propagates the growing type through the chain. @return built returns the final result with all accumulated fields.
Naming built types (@built-name)
By default, the built type inherits the schema's class name. Use @built-name to give it a custom name from a string argument:
---@built-name 1
---@return self
function Schema.Create(name) return Schema endlocal MY_SCHEMA = Schema.Create("PlayerState")
:AddString("name")
:AddNumber("level")
local state = MY_SCHEMA:Build()
-- state has type PlayerState { name: string, level: number? }The name PlayerState is registered globally — you can reference it in @param and @type annotations across files:
---@param state PlayerState
function process(state)
print(state.name) -- completion works
endExtending schemas (@built-extends)
Use @built-extends with @built-name to create a new type that inherits from the receiver's current built type:
---@param name string
---@built-name 1
---@built-extends
---@return self
function Schema:Extend(name) return self endlocal BASE = Schema.Create("BaseState")
:AddString("name")
:AddNumber("level")
local CHILD = BASE:Extend("ChildState")
:AddString("childField")
local inst = CHILD:Build()
inst.childField -- string (own field)
inst.name -- string (inherited from BaseState)
inst.level -- number (inherited from BaseState)Multi-level extension works — grandchild inherits from child and base.
Lateinit fields (T!)
Builder fields support the ! suffix for lateinit semantics:
---@generic T
---@param name string
---@param class T|`T`
---@builds-field 1 T!
---@return self
function Schema:AddDeferred(name, class) return self endFields created with T! allow nil assignment without field-type-mismatch but hover as non-nil. This is useful for object pools or lazy initialization patterns.
@class overlays
A @class declaration with the same name as a @built-name type merges its @field annotations with the builder fields. Overlay fields take precedence:
---@class PlayerState
---@field name string
---@field readonly computed stringIf PlayerState is also created via @built-name, the @field declarations overlay the built fields. This lets you declare computed or virtual fields that aren't part of the builder chain.
Inheriting from a parent class
@return built : ParentClass adds a parent class to the built type:
---@class State
---@field GetValue fun(self, key: string): any
---@return built : State
function Schema:CreateState() return {} end
local state = Schema:AddString("name"):CreateState()
state.name -- string (from builder)
state:GetValue() -- inherited from State