Skip to content

Basic Annotations ​

Annotations are special comments that tell the language server about your code's types. They use the ---@ prefix (three dashes, then @), which is the same syntax as LuaLS. If you've used LuaLS before, your annotations carry over; a few LuaLS-only tags such as @operator and @async are accepted but have no effect.

Documentation comments ​

A plain --- comment (three dashes, no @) directly above a declaration becomes its documentation, shown in the hover and signature help.

Your line structure is preserved: each line stays on its own line, and a blank --- line becomes a paragraph break. To split a tooltip across multiple lines, just write multiple --- lines:

lua
---Gets the locale table.
---Loaded lazily on first access.
---
---Keys are lowercase locale codes.
---@return table<string, string>
function Locale.GetTable() end

The hover renders as:

Gets the locale table.
Loaded lazily on first access.

Keys are lowercase locale codes.

Doc comments are rendered as Markdown, so you can also use **bold**, lists, inline `code`, and links. The doc block must sit directly above the declaration — a truly blank line (one with no ---) between them detaches it.

@param: Parameter types ​

Declare what a function expects:

lua
---@param name string
---@param level number
---@param guild string?
function registerPlayer(name, level, guild)
    -- name: string, level: number, guild: string | nil
end

The ? suffix makes a parameter optional (its type becomes T | nil). Callers that omit optional arguments won't get a missing-parameter warning.

When to use it ​

Annotate parameters on functions that are called from other files or that form your addon's API. For small local helpers, the LS can often infer parameter types from how they're used in the body - see backward inference below.

Common patterns ​

lua
-- Multiple types
---@param value string|number
function display(value) end

-- Callback parameter
---@param handler fun(item: string, count: number): boolean
function forEach(handler) end

-- Table shape parameter
---@param opts {name: string, verbose?: boolean}
function configure(opts) end

@return: Return types ​

Declare what a function returns:

lua
---@return string
function Player:GetName() return self.name end

Multiple return values ​

Lua functions can return multiple values. Use separate @return lines for each:

lua
---@return string name
---@return number level
function getInfo()
    return "Arthas", 80
end

local name, level = getInfo()
-- name: string, level: number

The names after the type (name, level) are labels - they show in hover and signature help but don't affect type checking.

Labeled tuple syntax ​

For multi-return functions, the tuple syntax is often clearer:

lua
---@return (string name, number level)
function getInfo()
    return "Arthas", 80
end

This is equivalent to separate @return lines but keeps the signature compact. Where it really shines is tuple-union returns for correlated values.

Optional returns ​

lua
---@return string?
function Player:MaybeGetName()
    if not self.loaded then return nil end
    return self.name
end

The caller sees string | nil and the LS will enforce nil checks if need-check-nil is enabled.

@type: Variable types ​

Force a variable's type:

lua
---@type AuctionEntry[]
local entries = {}

---@type number?
local cachedValue = nil

When a statement assigns several targets, list one type per target, in order, either above the statement or at the end of its line:

lua
---@type number, string
local count, label = GetCountAndLabel()

local addonName, ns = ... ---@type string, MyAddonNamespace

When to use @type

@type is most useful when the LS can't infer the type from the right-hand side: empty tables, nil initializers, or values from external APIs. For simple assignments like local x = 5, the LS already knows the type, so the annotation is optional.

Inline @as casts ​

When you need a type assertion on an expression (not a variable), use @as in a block comment:

lua
local frame = CreateFrame("Frame") --[[@as Frame]]
doSomething(value --[[@as string]])

@as is an escape hatch. It tells the LS "trust me, this is the type." Use it when you know more than the LS can infer, but prefer fixing the root cause (adding annotations upstream) when possible.

@cast: Modify a variable's type ​

@cast changes a variable's type from that point forward. It supports three modes:

lua
---@type string|number|nil
local x = getValue()

---@cast x string          -- replace: x is now string
---@cast x +boolean        -- add: x is now string | boolean
---@cast x -nil            -- remove: strip nil from x

@cast is useful after runtime checks that the LS can't follow - for example, after a custom validation function that you know guarantees a type.

Type syntax ​

Annotations use a rich type syntax for describing values:

SyntaxMeaning
string, number, boolean, nil, anyPrimitive types
tableAny table
functionAny function
A | BUnion: value is A or B
A & BIntersection: value is both A and B
T[]Array of T
T?Shorthand for T | nil
T!Non-nil assertion / lateinit (see Nil Safety)
table<K, V>Map with key type K, value type V
fun(a: T, b: U): RFunction type
{name: string, age: number}Anonymous table shape
"literal"String literal type
true, falseBoolean literal types
integerInteger subtype of number

Unions and intersections ​

Unions are the most common compound type. A value that might be a string or nil is string | nil (or string?):

lua
---@param name string|nil
function greet(name)
    if name then
        print("Hello " .. name) -- narrowed to string
    end
end

Intersections represent WoW's mixin pattern: a value that has the fields and methods of multiple types combined. The most common case is frames created with templates:

lua
---@param widget Frame & BackdropTemplate
function setupWidget(widget)
    widget:SetPoint("CENTER")  -- Frame method
    widget:SetBackdrop({})     -- BackdropTemplate mixin method
end

CreateFrame builds these types for you - no annotation needed at the call site: CreateFrame("Button", nil, nil, "BackdropTemplate") returns Button & BackdropTemplate. See Mixins and Templates for more.

Anonymous table shapes ​

When you don't need a named class, use an inline table shape:

lua
---@param opts {name: string, count: number, verbose?: boolean}
function create(opts)
    print(opts.name)     -- string
    print(opts.verbose)  -- boolean | nil
end

Optional fields use ? before the colon: verbose?. They allow nil.

Function types ​

Function types describe callable values:

lua
---@param callback fun(item: string, index: number): boolean
function filter(callback) end

---@type fun(): string, number
local getValues

Parameter names in function types are for documentation - they show in hover and signature help.

Backward inference ​

wowlua-ls can often figure out parameter types without annotations by analyzing how parameters are used in the function body. This is called backward inference.

lua
-- No annotations needed: the LS infers x is number from the arithmetic
local function double(x)
    return x * 2
end

---@param name string
local function greet(name) print("Hello " .. name) end

-- Infers player is string from the typed call
local function welcome(player)
    greet(player)
end

Backward inference works from:

  • Arithmetic and concatenation: x + 1 implies number, x .. "hi" implies string | number
  • Typed function arguments: if x is passed to a function expecting number, that's a hint
  • Unary operators: -x implies number, #x implies table | string
  • Table keys: prices[x] implies the table's key type when it's declared (table<string, number>) or comes from the table's constructor or list inserts. Keys written into a table later (seen[k] = true) don't type lookups, since a lookup with any other key just returns nil.

Conflicting usages

Backward inference treats each usage as an upper bound and intersects them. If a parameter is used in conflicting ways (passed to a function expecting string and another expecting number), the LS leaves it untyped rather than guessing wrong. When that happens, add an explicit @param.

The inference also bails when a function is called with incompatible types at different sites:

lua
local function register(frame)
    frame:Show()
end

register(GameTooltip)
register(ItemRefTooltip)
-- Two different frame types → inference bails, parameter stays untyped

This is intentional. The LS won't pick one caller's type and reject the other. Add @param frame Frame (or whatever the common base type is) to resolve it.