Generics
Generic type parameters let you write functions and classes that work with any type while preserving type information through calls.
Generic functions
Declare type parameters with @generic:
---@generic T
---@param value T
---@return T
function identity(value) return value end
local s = identity("hello") -- s: string
local n = identity(42) -- n: numberThe LS infers T from the argument and propagates it to the return type. No explicit type annotation needed at the call site.
Constrained generics
Restrict a type parameter to a specific class or type:
---@generic T: Frame
---@param frame T
---@return T
function configure(frame)
frame:SetPoint("CENTER")
return frame
end
local f = configure(CreateFrame("Frame")) -- T is Frame
configure("not a frame") -- type-mismatch: string doesn't satisfy FrameMultiple type parameters
---@generic K, V
---@param key K
---@param value V
---@return table<K, V>
function makePair(key, value)
return { [key] = value }
endBacktick annotations (`T`)
When a parameter is a string literal that names a class, use backticks to resolve it as a type:
---@generic T
---@param name `T`
---@return T
function CreateObject(name) return {} end
local dog = CreateObject("Dog") -- T resolves to the Dog classThis is how WoW's CreateFrame works — the first argument is a string like "Frame" or "Button", and the return type matches.
Parameterized classes
Classes can declare type parameters that their methods and fields reference:
---@class Container<T>
---@field items T[]
local Container = {}
---@param item T
function Container:Add(item) end
---@return T
function Container:Get() endWhen a value is typed with a concrete parameter, fields and methods resolve accordingly:
---@type Container<string>
local names = {}
names:Add("Arthas") -- T is string
local n = names:Get() -- n: string
names.items -- string[]Methods on a parameterized class automatically inherit the class-level type parameters — no need to redeclare them with @generic:
-- Just use T directly
---@param value T
function MyClass:Set(value) endType argument checking at call sites
When an argument and parameter are the same parameterized class (or the argument is a subclass that forwards the type parameter, e.g. Child<T> : Parent<T>), the type arguments are compared too — not just the class itself:
---@param c Container<boolean>
local function wantBool(c) end
---@type Container<number>
local nums = {}
wantBool(nums) -- type-mismatch: expected Container<boolean>, got Container<number>A union type argument is tolerated when any of its members is compatible, so truthiness idioms whose inferred value type is a union containing the expected type (e.g. Container<boolean | number> against Container<boolean>) don't warn. Positions whose expected or actual type argument is unconstrained (any or an unresolved generic) are skipped.
Parameterized inheritance
A subclass that forwards its type parameters to a parameterized parent inherits the parent's fields and methods with the parameter substituted, and is treated as a subtype for type-argument comparison:
---@class Box<T>
---@field _value T
local Box = {}
---@return T
function Box:Get() return self._value end
---@class BoolBox<T> : Box<T> -- forwards T to Box
local BoolBox = {}
---@type BoolBox<number>
local b = {}
local v = b:Get() -- v: number (inherited Box:Get with T = number)Only identity forwarding parents (Child<T> : Parent<T>, same parameters in the same order) are linked this way. A parent that binds a concrete type (Child<T> : Parent<string>) or reorders parameters is not registered as a parameterized subtype, since the type arguments can't be compared positionally.
Type parameter constraints
---@class NumericBox<T: number|string>
local NumericBox = {}
---@type NumericBox<string> -- OK
local a = {}
---@type NumericBox<boolean> -- generic-constraint-mismatch
local b = {}keyof constraints and indexed access types
The keyof T constraint restricts a type parameter to the field names of another type. Combined with T[K] (indexed access), this lets you write functions where both the key and the return type are validated:
---@class Config
---@field name string
---@field value number
---@field enabled boolean
---@generic T, K: keyof T
---@param obj T
---@param key K
---@return T[K]
local function getField(obj, key)
return obj[key]
end
---@type Config
local cfg = { name = "test", value = 42, enabled = true }
local n = getField(cfg, "name") -- n: string
local v = getField(cfg, "value") -- v: number
local e = getField(cfg, "enabled") -- e: boolean
getField(cfg, "bogus") -- generic-constraint-mismatch: "bogus" is not a field of ConfigThe LS also provides string literal completions for keyof-constrained parameters — typing getField(cfg, "") will suggest enabled, name, value.
This pattern is useful for WoW addon code that needs to safely access fields by name:
---@generic T, K: keyof T
---@param obj T
---@param method K
---@param ... any
function CallMethod(obj, method, ...)
obj[method](obj, ...)
endkeyof self
Inside a method, keyof self resolves to the field names of the call's receiver. This avoids declaring a separate generic for the receiver type when the only thing you need is its key set:
---@class Widget
local Widget = {}
function Widget:Show() end
function Widget:Hide() end
---@generic K: keyof self
---@param method K
function Widget:Dispatch(method)
self[method](self)
end
---@type Widget
local w = {}
w:Dispatch("Show") -- ok
w:Dispatch("Hide") -- ok
w:Dispatch("Nope") -- generic-constraint-mismatch: "Nope" is not a method of WidgetFor subclasses, the receiver's full surface (own + inherited methods) satisfies the constraint, and completions, references, and rename all see the resolved set. keyof self only fires for method calls (: syntax) — a direct function call where self is passed explicitly won't enforce the constraint.
Bracket-index fields
Type parameters work in bracket-index field declarations:
---@class TypedMap<K, V>
---@field [K] V
---@type TypedMap<string, number>
local scores = {}
local val = scores["player1"] -- val: numberFunction-type projections
When a generic class wraps a function type, params<F> and returns<F> let methods reference the function's shape:
---@class EventRegistry<F>
local EventRegistry = {}
---@generic F
---@param self EventRegistry<F>
---@param key string
---@param ... params<F>
---@return returns<F>
function EventRegistry:Fire(key, ...) end---@type EventRegistry<fun(name: string, count: number): boolean>
local reg = {}
local ok = reg:Fire("event", "hello", 5) -- ok: boolean
reg:Fire("event", 42, 5) -- type-mismatch: position 1 expects stringparams<F>is only valid in the vararg slot (@param ... params<F>)returns<F>resolves to F's return type
Projections in inline fun() return types
You can also use params<F> and returns<F> inside inline fun() type expressions. This is useful for function wrappers that transform signatures:
---@generic F
---@param func F
---@return fun(...: params<F>): string
local function wrapToString(func)
return function(...)
func(...)
return ""
end
end
---@param a number
---@param b string
---@return boolean
local function original(a, b) return true end
local wrapped = wrapToString(original)
-- wrapped is: fun(a: number, b: string): string---@generic F
---@param func F
---@return fun(key: string): returns<F>
local function wrapKeyed(func)
return function(key) return func(key) end
endVariadic generics
A variadic generic parameter collects any number of excess arguments into an intersection type. Declare one with ... prefix:
---@generic T, ...M
---@param object T
---@param ... any
---@return T & ...M
function Mixin(object, ...) endThe first argument binds T. All remaining arguments bind ...M as an intersection:
---@class Draggable
---@class Resizable
---@class Scrollable
---@type Frame
local f = {}
local result = Mixin(f, Draggable, Resizable, Scrollable)
-- result: Frame & Draggable & Resizable & ScrollableThere's no limit on the number of arguments — they all flow into the intersection.
A variadic generic can also be the only generic parameter:
---@generic ...M
---@param ... any
---@return ...M
function CreateFromMixins(...) end
local obj = CreateFromMixins(Draggable, Resizable)
-- obj: Draggable & ResizableWhen no excess arguments are provided, the variadic generic stays unbound and is filtered out, leaving just the non-variadic parts of the return type.
How inference works
The LS infers generic bindings from multiple sources:
- Direct argument types — if
@param x T, and you pass astring, T = string - Structural matching —
T[]extracts T from an array's element type;table<K,V>extracts from map types - Backtick resolution —
`T`resolves a string literal as a class name - Function-type extraction —
fun(): Textracts T from a callback's return type - Receiver type_args — for
@class Foo<T>, calling a method on---@type Foo<string>binds T from the receiver
Inference runs per-call and doesn't persist — each call site resolves its own bindings independently.
