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