The @generic annotation allows you to define generic type parameters, enabling code reuse while maintaining type safety and accurate type inference.
Syntax
---@generic <generic_name1>[: <constraint_type1>] [, <generic_name2>[: <constraint_type2>]...]
Basic Generic Function
Create a function that works with any type:
---@generic T
---@param value T Input value
---@return T Output value of same type
function identity(value)
return value
end
-- Usage
local str = identity("hello") -- str type is string
local num = identity(42) -- num type is number
local tbl = identity({a = 1}) -- tbl type is table
How it works:
@generic T declares a type parameter named T
- The parameter and return value both use type
T
- The language server infers the actual type from usage
- Each call can use a different concrete type
Generic functions provide type safety without sacrificing flexibility. The type is inferred from the argument, so you get accurate autocomplete on the return value.
Multiple Generic Parameters
Use multiple type parameters for functions working with different types:
---@generic K, V
---@param map table<K, V> Map table
---@return K[] Array of all keys
function getKeys(map)
local keys = {}
for k in pairs(map) do
table.insert(keys, k)
end
return keys
end
---@generic K, V
---@param map table<K, V> Map table
---@return V[] Array of all values
function getValues(map)
local values = {}
for _, v in pairs(map) do
table.insert(values, v)
end
return values
end
-- Usage
local ageMap = {John = 30, Jane = 25}
local names = getKeys(ageMap) -- string[]
local ages = getValues(ageMap) -- number[]
Line-by-line explanation:
- Line 1: Declare two generic parameters
K (key type) and V (value type)
- Line 2: Accept a table with keys of type
K and values of type V
- Line 3: Return an array of type
K[]
- The language server infers
K = string and V = number from the argument
Generic Constraints
Restrict generic types to specific base types:
---@generic T : table
---@param obj T Object that must be a table
---@return T Cloned object
function deepClone(obj)
local clone = {}
for k, v in pairs(obj) do
if type(v) == "table" then
clone[k] = deepClone(v)
else
clone[k] = v
end
end
return clone
end
-- Usage
local original = {a = 1, b = {c = 2}}
local copy = deepClone(original) -- OK: table type
local str = "hello"
local strCopy = deepClone(str) -- Warning: string doesn't satisfy table constraint
The constraint T : table means:
T can be any table type
- But it cannot be a string, number, or other non-table type
- This prevents misuse and provides better error messages
Constraints are enforced by the language server for type checking, but not at runtime. Always validate inputs if runtime safety is critical.
Generic Classes
Create reusable container classes with type parameters:
---@generic T
---@class Stack<T>
---@field private items T[]
local Stack = {}
---@generic T
---@return Stack<T>
function Stack.new()
return setmetatable({items = {}}, {__index = Stack})
end
---@param self Stack<T>
---@param item T
function Stack:push(item)
table.insert(self.items, item)
end
---@param self Stack<T>
---@return T?
function Stack:pop()
return table.remove(self.items)
end
---@param self Stack<T>
---@return number
function Stack:size()
return #self.items
end
Usage:
local stringStack = Stack.new() -- Stack<string>
stringStack:push("hello")
stringStack:push("world")
local str = stringStack:pop() -- str is string?
local numberStack = Stack.new() -- Stack<number>
numberStack:push(1)
numberStack:push(2)
local num = numberStack:pop() -- num is number?
Generic classes let you write container logic once and reuse it with different element types while maintaining type safety.
Array Operations
Implement type-safe functional array utilities:
---@generic T
---@param array T[] Array to filter
---@param predicate fun(item: T): boolean Filter predicate
---@return T[] Filtered array
function filter(array, predicate)
local result = {}
for _, item in ipairs(array) do
if predicate(item) then
table.insert(result, item)
end
end
return result
end
---@generic T, U
---@param array T[] Array to map
---@param mapper fun(item: T): U Mapping function
---@return U[] Mapped array
function map(array, mapper)
local result = {}
for _, item in ipairs(array) do
table.insert(result, mapper(item))
end
return result
end
---@generic T
---@param array T[] Array to reduce
---@param reducer fun(acc: T, item: T): T Reducer function
---@param initial T Initial value
---@return T Reduced value
function reduce(array, reducer, initial)
local acc = initial
for _, item in ipairs(array) do
acc = reducer(acc, item)
end
return acc
end
Usage examples:
local numbers = {1, 2, 3, 4, 5}
-- filter: number[] -> number[]
local evenNumbers = filter(numbers, function(n) return n % 2 == 0 end)
-- evenNumbers is number[] = {2, 4}
-- map: number[] -> number[]
local doubled = map(numbers, function(n) return n * 2 end)
-- doubled is number[] = {2, 4, 6, 8, 10}
local names = {"John", "Jane", "Bob"}
-- map: string[] -> number[]
local lengths = map(names, function(name) return #name end)
-- lengths is number[] = {4, 4, 3}
-- filter: string[] -> string[]
local longNames = filter(names, function(name) return #name > 3 end)
-- longNames is string[] = {"John", "Jane"}
-- reduce: number[] -> number
local sum = reduce(numbers, function(acc, n) return acc + n end, 0)
-- sum is number = 15
Line-by-line explanation for map:
- Line 1: Declare two generics:
T (input type) and U (output type)
- Line 2: Input is an array of
T
- Line 3: Mapper function transforms
T to U
- Line 4: Return an array of
U
- This allows transforming an array from one type to another
Practical Examples
Optional/Maybe Type
---@generic T
---@class Optional<T>
---@field private value T?
---@field private hasValue boolean
local Optional = {}
---@generic T
---@param value T?
---@return Optional<T>
function Optional.of(value)
return setmetatable({
value = value,
hasValue = value ~= nil
}, {__index = Optional})
end
---@param self Optional<T>
---@return boolean
function Optional:isPresent()
return self.hasValue
end
---@param self Optional<T>
---@return T
function Optional:get()
if not self.hasValue then
error("No value present")
end
return self.value
end
---@param self Optional<T>
---@param default T
---@return T
function Optional:orElse(default)
return self.hasValue and self.value or default
end
-- Usage
local opt = Optional.of("hello") -- Optional<string>
if opt:isPresent() then
print(opt:get()) -- "hello"
end
local empty = Optional.of(nil) -- Optional<nil>
print(empty:orElse("default")) -- "default"
Result Type
---@generic T, E
---@class Result<T, E>
---@field success boolean
---@field value T?
---@field error E?
local Result = {}
---@generic T, E
---@param value T
---@return Result<T, E>
function Result.ok(value)
return {success = true, value = value, error = nil}
end
---@generic T, E
---@param error E
---@return Result<T, E>
function Result.err(error)
return {success = false, value = nil, error = error}
end
---@param self Result<T, E>
---@return boolean
function Result:isOk()
return self.success
end
---@param self Result<T, E>
---@return T?
function Result:getValue()
return self.value
end
---@param self Result<T, E>
---@return E?
function Result:getError()
return self.error
end
-- Usage
---@param userId number
---@return Result<User, string>
function getUser(userId)
if userId > 0 then
return Result.ok({id = userId, name = "John"})
else
return Result.err("Invalid user ID")
end
end
local result = getUser(123)
if result:isOk() then
print("User:", result:getValue().name)
else
print("Error:", result:getError())
end
Pair Type
---@generic A, B
---@class Pair<A, B>
---@field first A
---@field second B
local Pair = {}
---@generic A, B
---@param first A
---@param second B
---@return Pair<A, B>
function Pair.new(first, second)
return {first = first, second = second}
end
-- Usage
local stringNumber = Pair.new("age", 30) -- Pair<string, number>
local boolString = Pair.new(true, "yes") -- Pair<boolean, string>
print(stringNumber.first) -- "age" (string)
print(stringNumber.second) -- 30 (number)
Find Function
---@generic T
---@param array T[]
---@param predicate fun(item: T): boolean
---@return T? Found item or nil
function find(array, predicate)
for _, item in ipairs(array) do
if predicate(item) then
return item
end
end
return nil
end
-- Usage
local users = {
{id = 1, name = "John"},
{id = 2, name = "Jane"},
{id = 3, name = "Bob"}
}
local user = find(users, function(u) return u.id == 2 end)
-- user is {id: number, name: string}?
if user then
print(user.name) -- "Jane"
end
Best Practices
- Use descriptive generic names:
T for type, K for key, V for value, E for error
- Add constraints when appropriate: Prevents misuse and gives better errors
- Document the purpose: Explain what types are expected for each generic parameter
- Keep generics simple: Don’t over-engineer with too many type parameters
- Use generics for containers: Arrays, maps, stacks, queues benefit most
- Combine with other annotations: Use with
@param, @return, @class for complete type coverage
Common Patterns
Factory Functions
---@generic T
---@param constructor fun(): T
---@param count number
---@return T[]
function createMany(constructor, count)
local items = {}
for i = 1, count do
items[i] = constructor()
end
return items
end
Swap Function
---@generic T
---@param a T
---@param b T
---@return T, T
function swap(a, b)
return b, a
end
local x, y = swap(1, 2) -- number, number
local s1, s2 = swap("a", "b") -- string, string
Generics shine when you’re building utilities and libraries. They allow you to write flexible, reusable code without sacrificing type safety.
Generic type parameters are resolved at each call site. If the language server can’t infer the type, it may fall back to unknown or any. Add explicit type annotations when needed.