Skip to main content
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:
.emmyrc.json
{
  "strict": {
    "requirePath": false,
    "typeCall": false,
    "arrayIndex": true,
    "metaOverrideFileDefine": true,
    "docBaseConstMatchBaseType": true
  }
}
requirePath
boolean
default:false
When enabled, require paths must start from specified root directories. Disabling allows flexible path resolution.
typeCall
boolean
default:false
When enabled, manual overload definitions are required for type calls. When disabled, the analyzer returns the self type automatically.
arrayIndex
boolean
default:true
Enforces strict adherence to array indexing rules. Helps catch out-of-bounds access patterns.
metaOverrideFileDefine
boolean
default:true
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:
.emmyrc.json
{
  "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:
.emmyrc.json
{
  "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"
    ]
  }
}

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

Type Checking Performance

For large projects, consider disabling type checking in generated or third-party code directories using workspace.ignoreDir.
.emmyrc.json
{
  "workspace": {
    "ignoreDir": [
      "build",
      "vendor",
      "node_modules"
    ]
  }
}

Troubleshooting Type Errors

False Positives

If the analyzer reports incorrect errors:
  1. Add ---@diagnostic disable-next-line for single-line suppression:
---@diagnostic disable-next-line: param-type-not-match
local result = problematicFunction(arg)
  1. 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

Build docs developers (and LLMs) love