The @type annotation allows you to specify concrete type information for variables, expressions, or objects in your Lua code. This enables better autocomplete, type checking, and documentation.
Syntax
---@type <type_expression>
Basic Types
Declare primitive types for variables:
---@type string
local userName = "John"
-- userName is now recognized as a string
---@type number
local userAge = 25
-- userAge is recognized as a number
---@type boolean
local isActive = true
-- isActive is recognized as a boolean
Adding type annotations helps the language server provide better autocomplete suggestions and catch type errors early.
Union Types
Define variables that can hold multiple types:
---@type string | number
local mixedValue = "Can be string or number"
-- mixedValue can be either a string or a number
---@type string?
local optionalString = nil
-- Equivalent to string | nil
-- The ? suffix is shorthand for "this type OR nil"
Intersection Types
Combine multiple types where values must satisfy all type requirements:
---@type T & U
local mergedValue = getMergedValue()
How intersection types work:
- Fields from both types are included
- If a field exists on both sides, its type becomes the intersection of the two types
- Conflicting field types often collapse to
never (e.g., {y: number} & {y: string} → y: never)
- Best used with classes or object types that define fields
For overwrite-style merges where one type should win, use Merge<T, U> (right wins) or Merge<U, T> (left wins) instead of intersection types.
Array Types
Declare arrays with specific element types:
---@type string[]
local nameList = {"John", "Jane", "Bob"}
-- Array of strings
---@type number[]
local scores = {95, 87, 92, 88}
-- Array of numbers
---@type (string | number)[]
local mixedArray = {"John", 25, "Jane", 30}
-- Array that can contain strings OR numbers
Dictionary Types
Define key-value mappings:
---@type table<string, number>
local ageMap = {
["John"] = 25,
["Jane"] = 30,
["Bob"] = 28
}
-- Keys are strings, values are numbers
---@type table<number, string>
local idToName = {
[1001] = "John",
[1002] = "Jane",
[1003] = "Bob"
}
-- Keys are numbers, values are strings
Tuple Types
Define fixed-length arrays with specific types at each position:
---@type [string, number, boolean]
local userInfo = {"John", 25, true}
-- First element is string, second is number, third is boolean
Table Literal Types
Define structured object types inline:
---@type {name: string, age: number, email: string}
local user = {
name = "John",
age = 25,
email = "[email protected]"
}
---@type {user: {id: number, name: string}, permissions: string[]}
local userWithPermissions = {
user = {id = 1001, name = "John"},
permissions = {"read", "write", "delete"}
}
-- Nested table structures are supported
Function Types
Declare function signatures:
---@type fun(x: number, y: number): number
local addFunction = function(x, y)
return x + y
end
-- Function takes two numbers and returns a number
---@type fun(name: string, age: number): {name: string, age: number}
local createUser = function(name, age)
return {name = name, age = age}
end
-- Function returns a table with name and age fields
---@type async fun(url: string): string
local fetchData = async function(url)
return await httpGet(url)
end
-- Async function that returns a string
Class Types
Reference custom class types defined with @class:
---@class User
---@field id number
---@field name string
---@type User
local currentUser = {
id = 1001,
name = "John"
}
---@type User[]
local userList = {
{id = 1001, name = "John"},
{id = 1002, name = "Jane"}
}
-- Array of User objects
Generic Types
Use parameterized types for flexible, reusable type definitions:
---@class Container<T>
---@field items T[]
---@type Container<string>
local stringContainer = {
items = {"hello", "world"}
}
-- Container holding strings
---@type Container<number>
local numberContainer = {
items = {1, 2, 3, 4, 5}
}
-- Container holding numbers
---@type table<string, Container<User>>
local userContainerMap = {
["admins"] = {items = {{id = 1, name = "Admin"}}},
["users"] = {items = {{id = 2, name = "Regular User"}}}
}
-- Complex generic combinations are supported
Enumeration Types
Use type aliases for enumeration-like values:
---@alias Status 'active' | 'inactive' | 'pending'
---@type Status
local currentStatus = 'active'
-- currentStatus can only be 'active', 'inactive', or 'pending'
Callback Types
Define callback function signatures:
---@type fun(error: string?, result: any?): nil
local callback = function(error, result)
if error then
print("Error:", error)
else
print("Result:", result)
end
end
-- Callback with optional error and result parameters
Practical Examples
Event Handlers
---@type table<string, fun(...)>
local eventHandlers = {
["click"] = function(x, y)
print("Click position:", x, y)
end,
["keypress"] = function(key)
print("Key pressed:", key)
end
}
Index Signatures
---@type {[string]: any}
local dynamicObject = {
someKey = "someValue",
anotherKey = 123,
yetAnother = true
}
-- Object with dynamic string keys
Type Narrowing in Conditionals
---@type User | nil
local user = isLoggedIn and getCurrentUser() or nil
if user then
-- In this block, user's type is User (non-nil)
print("Username:", user.name)
print("User ID:", user.id)
end
Type-Safe Loops
---@type string[]
local nameList = {"John", "Jane", "Bob"}
---@type string
for _, name in ipairs(nameList) do
print("Name:", name) -- name is inferred as string type
end
Common Patterns
Promise Types
---@class Promise<T>
---@field then fun(self: Promise<T>, onResolve: fun(value: T), onReject?: fun(error: any))
---@type Promise<string>
local dataPromise = fetchUserDataAsync(1001)
Readonly Types (by convention)
---@type {readonly name: string, readonly id: number}
local readonlyUser = {name = "John", id = 1001}
While Lua doesn’t enforce readonly at runtime, this convention helps document intent and some language servers may provide warnings when modifying readonly fields.
Best Practices
- Use union types for flexible parameters:
string | number is clearer than any
- Prefer specific types over
any: Type annotations are most useful when they’re specific
- Use optional types (
?) for nullable values: Makes nil-checking explicit
- Combine with
@class for complex structures: Define reusable types instead of repeating table literals
- Annotate public APIs: Focus on function parameters and return values that others will use
The @type annotation only provides type information to the language server. It does not perform runtime type checking or validation. Always validate user input at runtime.