Custom Type Guards
wowlua-ls narrows types automatically for built-in patterns like if x then, type(x) == "string", and assert(). But WoW addons often have their own type-checking functions. The @type-narrows annotation lets you teach the LS about them.
Index-based form
@type-narrows <target_param> <classname_param> — both are 1-based call-site argument positions:
---@param element UIElement
---@param typeName string
---@type-narrows 1 2
---@return boolean
function UI.IsType(element, typeName) endWhen used in a condition, the LS narrows the first argument to the class named by the second:
---@param parent UIElement
function example(parent)
if UI.IsType(parent, "ScrollFrame") then
parent._scrollbar -- parent is now ScrollFrame
end
-- Works with early exit too
if not UI.IsType(parent, "ScrollFrame") then return end
parent._scrollbar -- narrowed for the rest of the function
endUse 0 for the receiver (self) in colon method calls:
---@param typeName string
---@type-narrows 0 1
---@return boolean
function UIElement:IsType(typeName) end
if element:IsType("ScrollFrame") then
element._scrollbar -- narrowed
endThe classname argument can be either a string literal (e.g. "ScrollFrame") or a single-name identifier that matches a known @class (e.g. ScrollFrame — the class table itself). Dotted names like MyLib.Dog are not supported. This matches idiomatic class-library patterns like obj:isA(Class).
Method-style form
@type-narrows ClassName — narrows self to a fixed class. Useful for boolean predicate methods:
---@class AuctionRow
---@class AuctionSubRow : AuctionRow
---@field parentId number
---@type-narrows AuctionSubRow
---@return boolean
function AuctionRow:IsSubRow() return false end---@param row AuctionRow
function example(row)
if row:IsSubRow() then
row.parentId -- row is AuctionSubRow
end
assert(row:IsSubRow())
row.parentId -- also works with assert
endWhere narrowing applies
Custom type guards work in all the same places as built-in narrowing:
if guard() then ... end(then branch)if guard() then ... else ... end(both branches)if not guard() then return end(early exit)assert(guard())(rest of function)guard() and expr/guard() or expr(short-circuit)
In-place argument narrowing (@narrows-arg)
@narrows-arg N narrows the Nth argument's type to the function's return type when the call is a bare statement (not assigned to a variable). This is useful for functions that mutate a value in-place, like WoW's Mixin():
---@generic T, ...M
---@narrows-arg 1
---@param object T
---@param ... any
---@return T & ...M
function Mixin(object, ...) endWhen you call Mixin without capturing the return value, the first argument's type is narrowed:
---@type Frame
local frame = {}
Mixin(frame, DraggableMixin)
-- frame is now Frame & DraggableMixin
frame:StartDragging() -- no warningThe index is 1-based and refers to call-site argument position (not counting self). Only bare function call statements trigger the narrowing — assignments like local x = Mixin(f, M) use the return type instead.
Literal boolean discrimination
A related feature that doesn't need @type-narrows: when union member types have methods returning literal true or false, the LS discriminates automatically:
---@class BaseRow
---@return false
function BaseRow:IsSubRow() return false end
---@class SubRow
---@return true
function SubRow:IsSubRow() return true end
---@param row BaseRow | SubRow
function handle(row)
if row:IsSubRow() then
row -- SubRow (returns true)
else
row -- BaseRow (returns false)
end
endNo @type-narrows needed — the literal boolean return types are enough. Requirements:
- All union members must define the method
- Every return must be literal
trueor literalfalse(not genericboolean) - At least one of each
Field presence narrowing
When a variable is typed as a union of classes, checking a field's truthiness narrows the variable to only the union members where that field is required (non-optional). This lets you model mutually exclusive fields without boilerplate:
---@class ScrollTable.ColInfo.WithTitle
---@field title string
---@field font string
---@class ScrollTable.ColInfo.WithIcon
---@field titleIcon string
---@field font string
---@alias ScrollTable.ColInfo ScrollTable.ColInfo.WithTitle | ScrollTable.ColInfo.WithIcon
---@param col ScrollTable.ColInfo
function setupColumn(col)
if col.title then
col -- ScrollTable.ColInfo.WithTitle
print(col.title) -- string, not string?
else
col -- ScrollTable.ColInfo.WithIcon
print(col.titleIcon) -- string, not string?
end
endThe LS splits the union: members where the checked field is required go to the then-branch; members where it's absent or optional go to the else-branch.
This works everywhere narrowing applies:
---@param col ScrollTable.ColInfo
function example(col)
-- Early exit
if not col.title then return end
col -- WithTitle for the rest of the function
-- Nil comparison
if col.title ~= nil then ... end
-- Assert
assert(col.title)
col -- WithTitle
endRules:
- A field is "required" if it exists on the class and is non-optional (no
?suffix) - A field is "absent or optional" if the class doesn't define it, or defines it with
? - If all union members have the field as required, no narrowing occurs (can't discriminate)
- Works with unions of 2+ members — narrows to whichever subset has the field
