Skip to content

Instantly share code, notes, and snippets.

@okikio
Last active September 1, 2025 04:34
Show Gist options
  • Select an option

  • Save okikio/61f0e5f16973db8cc4366ab57467a16b to your computer and use it in GitHub Desktop.

Select an option

Save okikio/61f0e5f16973db8cc4366ab57467a16b to your computer and use it in GitHub Desktop.
Lua for Typescript devs.

Lua for TypeScript Developers: The Complete Mental Model Shift

The Philosophy Shift: From Rigid Structure to Fluid Adaptability

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.

Core Mental Model: Everything is a Table (Almost)

The Table Revolution

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.

The Table as Everything Pattern

-- 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

Variable Declaration and Scoping: The local Lifeline

Global vs Local: The Silent Disaster

-- 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.

Functions: First-Class Citizens with Personality

Function Declaration Styles

-- 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 Return Values: Lua's Party Trick

-- 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 Manipulation: The Concatenation Chronicles

-- 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

Control Flow: Familiar Yet Different

If Statements and Truthiness

-- 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"

Loops: The Iteration Trio

-- 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

Error Handling: The pcall Paradigm

-- 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)

Modules and Require: The Import System

Creating a Module

-- 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

Using a 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)
end

Metatables: Lua's Secret Weapon

Think 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()) -- 5

Common Patterns and Idioms

The Factory Pattern

local 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 started

The Builder Pattern

local 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

Performance and Memory Considerations

Table Preallocation

-- 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

String Concatenation Performance

-- 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)

Common Gotchas for TypeScript Developers

1. Array Indexing

local arr = {"a", "b", "c"}
print(arr[0]) -- nil (not "a")
print(arr[1]) -- "a"

2. Length Operator

local arr = {"a", "b", "c", nil, "e"}
print(#arr) -- 3 (stops at first nil)

3. Global Variable Creation

-- Accidentally creates global
function test()
  myVar = "oops" -- Global!
  local myVar = "safe" -- Local
end

4. Truthiness

if 0 then print("zero is truthy") end -- This prints!
if "" then print("empty string is truthy") end -- This prints too!

Real-World Example: HTTP Server Module

-- 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 M

Usage:

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"

The Lua Mindset: Embrace the Flexibility

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:

  1. Practice the table patterns—they're fundamental
  2. Build small modules to understand the require system
  3. Experiment with metatables for advanced behavior
  4. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment