Think of TypeScript as a well-organized corporate office building—everything has its place, types are clearly labeled, and the compiler acts like a strict building inspector ensuring code safety. Lua, by contrast, is like a nimble startup garage where tables can morph into anything you need, functions are first-class citizens that hitchhike between contexts, and the runtime trusts you to make smart decisions.
This flexibility is both Lua's superpower and its potential pitfall. You're moving from a world of compile-time safety nets to runtime agility.
In TypeScript, you have objects, arrays, maps, sets, classes, and interfaces. In Lua, you have tables—and they're shape-shifters that can become any of these:
-- Arrays (1-indexed, because Lua says so)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- "apple" (NOT fruits[0])
-- Objects/Hash maps
local person = {
name = "Alice",
age = 30,
greet = function(self)
return "Hello, I'm " .. self.name
end
}
-- Mixed mode (array + object properties)
local hybrid = {"first", "second", x = 10, y = 20}
print(hybrid[1]) -- "first"
print(hybrid.x) -- 10
-- Metatables (think custom prototypes)
local mt = {
__index = function(t, key)
return "Dynamic value for " .. key
end
}
setmetatable(person, mt)
print(person.nonexistent) -- "Dynamic value for nonexistent"🚨 TypeScript Developer Gotcha: Arrays start at index 1, not 0. This will bite you. A lot.
-- Table as a class-like structure
local function createCounter(initial)
local self = {
value = initial or 0
}
function self:increment()
self.value = self.value + 1
end
function self:get()
return self.value
end
return self
end
local counter = createCounter(5)
counter:increment() -- Note the colon syntax for self
print(counter:get()) -- 6-- DANGER: Creates global variable
name = "Alice"
-- SAFE: Creates local variable
local name = "Alice"
-- Function parameters are automatically local
local function greet(name) -- `name` is local here
local greeting = "Hello " .. name -- `greeting` is local
return greeting
end
-- Block scoping with do...end
do
local temp = "I only exist in this block"
print(temp) -- Works
end
-- print(temp) -- Error: temp is nil🎯 Pro Tip: Always use local unless you explicitly need global scope. Global variables in Lua are like leaving your front door unlocked—convenient but dangerous.
-- Traditional declaration
local function add(a, b)
return a + b
end
-- Function expression (like TypeScript arrow functions)
local multiply = function(a, b)
return a * b
end
-- Higher-order functions
local function createOperation(op)
return function(a, b)
if op == "add" then
return a + b
elseif op == "multiply" then
return a * b
end
end
end
local adder = createOperation("add")
print(adder(3, 4)) -- 7-- Multiple returns (TypeScript would need tuples)
local function getDimensions()
return 1920, 1080, 32 -- width, height, depth
end
local width, height, depth = getDimensions()
print(width) -- 1920
-- Variadic functions
local function sum(...)
local args = {...} -- Pack arguments into table
local total = 0
for i = 1, #args do
total = total + args[i]
end
return total
end
print(sum(1, 2, 3, 4, 5)) -- 15-- String concatenation with .. (not +)
local greeting = "Hello" .. " " .. "World"
-- String interpolation doesn't exist (until Lua 5.4's string.format)
local name = "Alice"
local age = 30
local message = string.format("Name: %s, Age: %d", name, age)
-- Multi-line strings
local sql = [[
SELECT * FROM users
WHERE age > 18
AND status = 'active'
]]
-- String methods (different from JavaScript/TypeScript)
local text = " Hello World "
print(string.lower(text)) -- " hello world "
print(string.gsub(text, "%s+", "")) -- "HelloWorld" (regex-like patterns)
print(text:lower()) -- Method syntax sugar-- if/elseif/else/end structure
local function checkValue(x)
if x > 0 then
return "positive"
elseif x < 0 then
return "negative"
else
return "zero"
end
end
-- Truthiness: Only nil and false are falsy
local function isTruthy(value)
if value then
return "truthy"
else
return "falsy"
end
end
print(isTruthy(0)) -- "truthy" (different from JS!)
print(isTruthy("")) -- "truthy" (different from JS!)
print(isTruthy(nil)) -- "falsy"
print(isTruthy(false)) -- "falsy"-- Numeric for loop
for i = 1, 10 do
print(i)
end
-- For loop with step
for i = 1, 10, 2 do -- 1, 3, 5, 7, 9
print(i)
end
-- Generic for loop (like for...of in TypeScript)
local fruits = {"apple", "banana", "cherry"}
for index, fruit in ipairs(fruits) do
print(index, fruit) -- 1 apple, 2 banana, 3 cherry
end
-- Key-value iteration (like for...in in TypeScript)
local person = {name = "Alice", age = 30, city = "NYC"}
for key, value in pairs(person) do
print(key, value)
end
-- While loop
local i = 1
while i <= 5 do
print(i)
i = i + 1
end-- pcall (protected call) - like try/catch
local function riskyOperation(x)
if x < 0 then
error("Negative numbers not allowed")
end
return math.sqrt(x)
end
local success, result = pcall(riskyOperation, -5)
if success then
print("Result:", result)
else
print("Error:", result) -- result contains error message
end
-- xpcall with custom error handler
local function errorHandler(err)
return "Custom error: " .. err
end
local success, result = xpcall(riskyOperation, errorHandler, -5)-- mathutils.lua
local M = {} -- Module table
-- Private function (not exported)
local function isPositive(x)
return x > 0
end
-- Public functions
function M.add(a, b)
return a + b
end
function M.multiply(a, b)
return a * b
end
function M.safeDiv(a, b)
if b == 0 then
return nil, "Division by zero"
end
return a / b
end
return M -- Export the module-- main.lua
local math = require("mathutils")
print(math.add(3, 4)) -- 7
local result, err = math.safeDiv(10, 0)
if err then
print("Error:", err)
else
print("Result:", result)
endThink of metatables as custom behavior contracts. They're like TypeScript's Proxy objects but more fundamental to the language.
-- Creating a "class-like" structure with metatables
local Vector = {}
Vector.__index = Vector -- When key not found, look in Vector
function Vector.new(x, y)
local self = {x = x or 0, y = y or 0}
setmetatable(self, Vector)
return self
end
function Vector:magnitude()
return math.sqrt(self.x^2 + self.y^2)
end
-- Operator overloading
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__tostring(v)
return string.format("Vector(%.2f, %.2f)", v.x, v.y)
end
-- Usage
local v1 = Vector.new(3, 4)
local v2 = Vector.new(1, 2)
local v3 = v1 + v2 -- Uses __add metamethod
print(v1) -- Uses __tostring metamethod
print(v1:magnitude()) -- 5local function createLogger(level)
local self = {
level = level or "INFO"
}
function self:log(message)
print("[" .. self.level .. "] " .. message)
end
function self:setLevel(newLevel)
self.level = newLevel
end
return self
end
local logger = createLogger("DEBUG")
logger:log("System started") -- [DEBUG] System startedlocal function createQueryBuilder()
local self = {
_select = "*",
_from = "",
_where = {},
_limit = nil
}
function self:select(fields)
self._select = fields
return self -- Method chaining
end
function self:from(table)
self._from = table
return self
end
function self:where(condition)
table.insert(self._where, condition)
return self
end
function self:limit(n)
self._limit = n
return self
end
function self:build()
local query = "SELECT " .. self._select .. " FROM " .. self._from
if #self._where > 0 then
query = query .. " WHERE " .. table.concat(self._where, " AND ")
end
if self._limit then
query = query .. " LIMIT " .. self._limit
end
return query
end
return self
end
-- Usage
local query = createQueryBuilder()
:select("name, age")
:from("users")
:where("age > 18")
:where("status = 'active'")
:limit(10)
:build()
print(query) -- SELECT name, age FROM users WHERE age > 18 AND status = 'active' LIMIT 10-- Inefficient: Growing table dynamically
local list = {}
for i = 1, 10000 do
list[i] = i * i
end
-- More efficient: Pre-sized table (Lua 5.1+)
local list = {}
for i = 1, 10000 do
list[i] = i * i
end-- Inefficient for many concatenations
local result = ""
for i = 1, 1000 do
result = result .. "item" .. i
end
-- Efficient: Use table.concat
local parts = {}
for i = 1, 1000 do
parts[i] = "item" .. i
end
local result = table.concat(parts)local arr = {"a", "b", "c"}
print(arr[0]) -- nil (not "a")
print(arr[1]) -- "a"local arr = {"a", "b", "c", nil, "e"}
print(#arr) -- 3 (stops at first nil)-- Accidentally creates global
function test()
myVar = "oops" -- Global!
local myVar = "safe" -- Local
endif 0 then print("zero is truthy") end -- This prints!
if "" then print("empty string is truthy") end -- This prints too!-- httpserver.lua
local M = {}
-- Private state
local routes = {}
local middleware = {}
-- Route registration
function M.get(path, handler)
routes["GET:" .. path] = handler
end
function M.post(path, handler)
routes["POST:" .. path] = handler
end
-- Middleware system
function M.use(middlewareFunc)
table.insert(middleware, middlewareFunc)
end
-- Request processing
function M.handleRequest(method, path, data)
local request = {
method = method,
path = path,
data = data,
headers = {}
}
local response = {
status = 200,
headers = {},
body = ""
}
-- Run middleware
for _, mw in ipairs(middleware) do
local continue = mw(request, response)
if not continue then
return response
end
end
-- Find and execute route handler
local routeKey = method .. ":" .. path
local handler = routes[routeKey]
if handler then
handler(request, response)
else
response.status = 404
response.body = "Not Found"
end
return response
end
return MUsage:
local server = require("httpserver")
-- Add logging middleware
server.use(function(req, res)
print("Request: " .. req.method .. " " .. req.path)
return true -- Continue processing
end)
-- Define routes
server.get("/", function(req, res)
res.body = "Hello World"
end)
server.post("/users", function(req, res)
res.body = "User created: " .. (req.data or "no data")
end)
-- Handle requests
local response = server.handleRequest("GET", "/", nil)
print(response.body) -- "Hello World"Lua's power lies in its simplicity and flexibility. Where TypeScript gives you guardrails and compile-time safety, Lua gives you a minimal, fast runtime that trusts your judgment. It's the difference between driving a modern car with lane assist and stability control versus driving a responsive sports car that requires skill but offers pure performance.
The key is learning to think in tables, embrace the dynamism, and build your own safety through testing and careful design rather than relying on a type system to catch errors.
Next Steps:
- Practice the table patterns—they're fundamental
- Build small modules to understand the require system
- Experiment with metatables for advanced behavior
- Focus on local variable discipline from day one
Remember: In TypeScript, the compiler is your safety net. In Lua, discipline and testing are your safety nets. The trade-off is runtime speed and incredible flexibility.