EmmyLua Analyzer provides powerful static type checking capabilities to catch errors before runtime. This guide covers advanced type checking configurations and best practices for large codebases.
Strict Mode Configuration
Strict mode controls the strictness of type checking and code analysis. Configure it in .emmyrc.json:
{
"strict" : {
"requirePath" : false ,
"typeCall" : false ,
"arrayIndex" : true ,
"metaOverrideFileDefine" : true ,
"docBaseConstMatchBaseType" : true
}
}
When enabled, require paths must start from specified root directories. Disabling allows flexible path resolution.
When enabled, manual overload definitions are required for type calls. When disabled, the analyzer returns the self type automatically.
Enforces strict adherence to array indexing rules. Helps catch out-of-bounds access patterns.
When enabled, meta definitions (from annotations) override definitions in files. Set to false for behavior similar to luals.
Type Inference Configuration
EmmyLua’s type inference system automatically deduces types from your code. Fine-tune its behavior:
Type Narrowing
Type narrowing automatically refines types based on control flow:
--- @type string | nil
local value = getValue ()
if value then
-- Type narrowed to string here
print ( value : upper ())
end
The analyzer supports advanced narrowing with and/or operators and field checks on union types.
Field-Based Type Narrowing
Narrow union types by checking fields:
--- @class Cat
--- @field meow fun ()
--- @class Dog
--- @field bark fun ()
--- @param pet Cat | Dog
function handlePet ( pet )
if pet . meow then
-- Type narrowed to Cat
pet . meow ()
else
-- Type narrowed to Dog
pet . bark ()
end
end
Nil Checking
Configure nil checking diagnostics:
{
"diagnostics" : {
"severity" : {
"need-check-nil" : "warning"
}
}
}
--- @type string ?
local nullable = getMaybeString ()
-- Warning: need nil check
print ( nullable : upper ())
-- Correct: nil check performed
if nullable then
print ( nullable : upper ())
end
Handling Dynamic Types
Lua’s dynamic nature sometimes requires special handling:
Using any Type
--- @type any
local dynamic = loadDynamicConfig ()
-- No type checking on any
dynamic . foo . bar . baz ()
Use any sparingly. It disables type checking and reduces code safety.
Type Casting
Use ---@cast for explicit type conversion:
--- @type unknown
local data = parseJSON ( input )
--- @cast data table<string , string>
for key , value in pairs ( data ) do
print ( key , value : upper ())
end
Generic Functions
Preserve type information with generics:
--- @generic T
--- @param value T
--- @return T
function identity ( value )
return value
end
-- Type preserved: string
local str = identity ( "hello" )
-- Type preserved: number
local num = identity ( 42 )
Advanced Diagnostics Configuration
Customize which type errors are reported:
{
"diagnostics" : {
"enable" : true ,
"severity" : {
"param-type-not-match" : "warning" ,
"return-type-mismatch" : "error" ,
"assign-type-mismatch" : "warning" ,
"missing-return" : "warning" ,
"undefined-field" : "hint"
},
"disable" : [
"unused" ,
"redundant-parameter"
]
}
}
Key Type-Related Diagnostics
Best Practices for Large Codebases
1. Progressive Type Adoption
Start with loose typing and gradually add annotations:
-- Phase 1: No annotations
function processUser ( user )
return user . name
end
-- Phase 2: Add basic types
--- @param user table
--- @return string
function processUser ( user )
return user . name
end
-- Phase 3: Full type safety
--- @class User
--- @field name string
--- @field email string
--- @param user User
--- @return string
function processUser ( user )
return user . name
end
2. Use Type Aliases
Define reusable type aliases for complex types:
--- @alias UserId number
--- @alias UserMap table<UserId , User>
--- @alias Callback fun ( success : boolean , error : string ?)
--- @type UserMap
local users = {}
--- @param callback Callback
function fetchData ( callback )
callback ( true , nil )
end
3. Document Return Types
Always document function return types, especially for exported APIs:
--- @param x number
--- @param y number
--- @return number sum The sum of x and y
--- @return number product The product of x and y
function calculate ( x , y )
return x + y , x * y
end
4. Use Visibility Modifiers
Control access to internals:
--- @class MyClass
local MyClass = {}
--- @private
--- @type table<string , any>
MyClass . _cache = {}
--- @protected
function MyClass : _internalMethod ()
return self . _cache
end
---@public
function MyClass : publicMethod ()
return self : _internalMethod ()
end
5. Leverage Union and Intersection Types
--- @alias SuccessResult { success : true , data : any }
--- @alias ErrorResult { success : false , error : string }
--- @alias Result SuccessResult | ErrorResult
--- @return Result
function fetchData ()
if math.random () > 0.5 then
return { success = true , data = {} }
else
return { success = false , error = "Failed" }
end
end
--- @param result Result
function handleResult ( result )
if result . success then
-- Type narrowed to SuccessResult
print ( result . data )
else
-- Type narrowed to ErrorResult
print ( result . error )
end
end
For large projects, consider disabling type checking in generated or third-party code directories using workspace.ignoreDir.
{
"workspace" : {
"ignoreDir" : [
"build" ,
"vendor" ,
"node_modules"
]
}
}
Troubleshooting Type Errors
False Positives
If the analyzer reports incorrect errors:
Add ---@diagnostic disable-next-line for single-line suppression:
--- @diagnostic disable-next-line : param-type-not-match
local result = problematicFunction ( arg )
Use ---@diagnostic disable for file-wide suppression:
--- @diagnostic disable : undefined-field
-- Rest of file...
Type Definition Conflicts
When multiple type definitions conflict:
--- @class Config
--- @field timeout number
-- Later in another file...
--- @class Config -- Conflict !
--- @field retries number
-- Solution: Use class extension
--- @class ExtendedConfig : Config
--- @field retries number
Next Steps
Code Style Configure code style rules and formatting
Performance Optimize analyzer performance for large projects