Skip to main content
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

  1. Use descriptive generic names: T for type, K for key, V for value, E for error
  2. Add constraints when appropriate: Prevents misuse and gives better errors
  3. Document the purpose: Explain what types are expected for each generic parameter
  4. Keep generics simple: Don’t over-engineer with too many type parameters
  5. Use generics for containers: Arrays, maps, stacks, queues benefit most
  6. 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.

Build docs developers (and LLMs) love