luaguides

Teal: Typed Lua That Compiles to Lua

Lua is dynamically typed. For small scripts, that is fine. For large codebases, it gets harder to track what functions expect and what they return — typos become runtime errors, refactors break silently, and onboarding new developers requires reading code carefully to understand data shapes.

Teal addresses this by adding a type system to Lua, then compiling that typed code back to plain Lua. The result is a Lua dialect where you can catch type errors before running, but the output runs anywhere standard Lua runs: Lua 5.1 through 5.4, and LuaJIT.

Teal was created by Hisham Muhammad (the creator of LuaRocks), inspired by his experience maintaining large Lua applications at Kong and LuaRocks. The compiler itself is written in Teal.

Installation

luarocks install tl

This puts the tl command in your path. Teal has no runtime dependencies — the compiler is a single tl.lua file you can also load directly into a Lua project.

Your First Typed Code

Here is a simple Lua function:

local function add(a, b)
    return a + b
end

In Teal, you annotate the types:

local function add(a: number, b: number): number
    return a + b
end

Run it directly:

tl run add.tl

Or compile it to Lua:

tl gen add.tl
# produces add.lua

The generated add.lua is just plain Lua — no Teal dependency at runtime.

Type Annotations

Variables need explicit types in Teal. You annotate at declaration:

local x: number = 10
local name: string = "Alice"
local items: {string} = {"one", "two", "three"}
local ok: boolean = true

If you omit the type annotation, Teal infers it from the value. If you omit the value, you must annotate:

local count: integer  -- type is required without an initializer
count = 10  -- ok

Records: Structuring Data

Teal uses record to define structured types (similar to structs):

local record Point
    x: number
    y: number
end

local record Rectangle
    x: number
    y: number
    width: number
    height: number
end

local function area(r: Rectangle): number
    return r.width * r.height
end

local r: Rectangle = { x = 0, y = 0, width = 10, height = 5 }
print(area(r))  -- 50

Records are structural — if a table has the right fields, it satisfies the type, even if it was not created with the record constructor.

Interfaces

Use interface for types that describe what methods an object must have:

local interface Drawable
    draw: function(self: Drawable)
    x: number
    y: number
end

local record Circle
    x: number
    y: number
    radius: number
end

function Circle:draw()
    print("drawing circle at " .. self.x .. "," .. self.y)
end

local function render(d: Drawable)
    d:draw()
end

Generics

Write functions that work over multiple types:

local function identity<T>(x: T): T
    return x
end

print(identity<number>(42))      -- 42
print(identity<string>("hello")) -- hello

Generics also work with records:

local record Box<T>
    value: T
end

local function wrap<T>(x: T): Box<T>
    return { value = x }
end

Optional Arguments

Add ? to an argument name to make it optional:

local function greet(name: string, greeting?: string)
    local g = greeting or "Hello"
    print(g .. ", " .. name .. "!")
end

greet("Alice")                 -- Hello, Alice!
greet("Bob", "Good morning")  -- Good morning, Bob!

The ? belongs to the argument name, not the type. An optional argument may be omitted or passed as nil.

Enums

Define a fixed set of named values:

local enum Direction
    "north"
    "east"
    "south"
    "west"
 end

local function move(dir: Direction): Direction
    if dir == "north" then return "south"
    elseif dir == "east" then return "west"
    end
    return dir
end

Enums in Teal are string-typed at runtime.

Arrays and Maps

Teal arrays use {type} syntax:

local nums: {number} = {1, 2, 3, 4, 5}
local names: {string} = {"Alice", "Bob", "Carol"}

Maps use {keytype: valuetype}:

local scores: {string: number} = {
    Alice = 100,
    Bob = 85,
    Carol = 92
}

Function Types

Type annotations for functions, not just function declarations:

local type Comparator<T> = function(T, T): boolean

local function max<T>(a: T, b: T, cmp: Comparator<T>): T
    if cmp(a, b) then
        return a
    end
    return b
end

This is especially useful for callbacks and higher-order functions.

The Compiler CLI

Once tl is installed:

tl run script.tl       # run directly
tl check module.tl     # type-check and report errors, don't run
tl gen module.tl       # compile to module.lua (types stripped)
tl warnings            # list all available warnings

For project-level settings, create a tlconfig.lua file at your project root.

Loading Teal from Lua

Instead of precompiling, you can load .tl files at runtime:

local tl = require("tl")
tl.loader()  -- registers the .tl loader

-- Now require() works with .tl files
local mymodule = require("mymodule")  -- loads mymodule.tl

This is useful when you want to distribute .tl source and let the end user’s Lua environment compile it on the fly.

Declaration Files for External Libraries

Teal supports declaration files to type-check code that uses third-party Lua libraries. These are analogous to TypeScript’s .d.ts files:

# The teal-types repository has shared declaration files
https://github.com/teal-language/teal-types

You write a .d.tl file declaring the types of the library you are using, and Teal uses it for type checking.

Type Aliasing: Nominal vs Structural

When you write local type MyPoint = Point, you create a type alias — MyPoint and Point are the same type. When you write local record MyPoint end (or local type MyPoint = record ... end), you create a distinct nominal type, even if the fields match.

This matters when you want two records with identical shapes to be distinct:

local record Point3D
    x: number; y: number; z: number
end

local record Vector3D
    x: number; y: number; z: number
end

local p: Point3D = { x = 1, y = 2, z = 3 }
local v: Vector3D = p  -- compile error: Point3D is not a Vector3D

Both have the same shape, but Teal treats them as different nominal types. Use local type aliasing if you want them to be interchangeable.

When Teal Is a Better Fit Than Luau

Luau (Roblox’s typed Lua) is great for Roblox development but not portable. Teal produces standard Lua that runs anywhere:

  • OpenResty / Nginx Lua — type-check your API handlers
  • Game engines — as long as the engine supports standard Lua
  • Embedded Lua — type-check before deploying to constrained environments
  • Existing Lua codebases — add Teal incrementally to catch bugs

Teal also has no runtime dependency: pre-compile .tl to .lua and distribute only the Lua output.

See Also

  • lua-moonscript-intro — another compile-to-Lua language, focused on cleaner syntax rather than types
  • lua-closures — closures, used heavily in Teal code especially for callbacks and higher-order functions
  • lua-metatables — the table-level OOP model that Teal’s records and interfaces build on top of