type()
type(value) type() is a built-in function that returns the type name of any Lua value as a string. It works on any value — numbers, strings, tables, functions, booleans, nil, and userdata. It is the standard way to do runtime type checking in Lua, replacing the need for manual string comparison or complex conditional logic.
Return Values
type() always returns one of eight strings:
| Return value | Meaning |
|---|---|
"nil" | The value is nil |
"number" | The value is a number (integer or float) |
"string" | The value is a string |
"boolean" | The value is true or false |
"table" | The value is a table |
"function" | The value is a function |
"thread" | The value is a coroutine |
"userdata" | The value is userdata |
type(nil) -- "nil"
type(42) -- "number"
type("hello") -- "string"
type(true) -- "boolean"
type({}) -- "table"
type(type) -- "function"
type(coroutine.create(print)) -- "thread"
type(io.stdout) -- "userdata" (standard library)
Basic type checking
The eight return values give you a complete vocabulary for reasoning about your data at runtime. Once you know a value’s type, you can branch on it with standard if/elseif chains — no pattern matching or type-switch syntax needed. The example below builds a describe function that inspects a value and returns a human-readable summary, demonstrating how type() slots naturally into Lua’s conditional structures.
function describe(value)
local t = type(value)
if t == "nil" then
return "no value"
elseif t == "number" then
return string.format("number: %.2f", value)
elseif t == "string" then
return string.format("string of length %d", #value)
elseif t == "table" then
return string.format("table with %d keys", #value)
else
return t
end
end
describe(nil) -- "no value"
describe(3.14) -- "number: 3.14"
describe("hello") -- "string of length 5"
describe({a = 1}) -- "table with 0 keys"
Guard Clauses
The describe function shows how type() drives branching logic, but an equally important use is validating inputs at function boundaries. When a function expects a number and receives a string, the resulting error is often cryptic — attempt to perform arithmetic on a string value — and it surfaces far from the original call site.
A guard clause with type() catches the mismatch early and produces a clear error message that names the offending argument and the expected type. This is the conventional Lua approach for argument validation, matching how built-in functions like io.write or table.insert report type errors.
function add(a, b)
if type(a) ~= "number" then
error(string.format("bad argument #1 to 'add' (number expected, got %s)", type(a)))
end
if type(b) ~= "number" then
error(string.format("bad argument #2 to 'add' (number expected, got %s)", type(b)))
end
return a + b
end
add("hello", 2) -- error: bad argument #1 to 'add' (number expected, got string)
add(1, 2) -- 3
This pattern — checking the type and raising an error with the actual type — is the conventional Lua approach for argument validation, matching how built-in functions like io.write or table.insert report type errors.
Table type vs empty table
Note that an empty table still has type "table":
type({}) -- "table"
type({1, 2, 3}) -- "table"
type({a = 1}) -- "table"
There is no separate “empty table” type in Lua — every table, regardless of its contents, returns "table". To check whether a table has any entries, use next(t) == nil for all-keyed tables or #t == 0 for array-like ones. The distinction matters because type() alone cannot tell you whether a table is populated, and mistaking an empty table for a different kind of value is a common source of bugs in data-processing code.
Comparing Types
When comparing types, always use "string" comparison rather than checking against the type function directly:
-- Correct
if type(x) == "number" then ...
-- Incorrect (compares to the type function itself, not its result)
if type(x) == number then ...
number without quotes is a variable name, not a string. Lua interprets it as an identifier — if number is undefined, the comparison fails with a confusing error like attempt to compare string with nil. Always quote the type names: "number", "string", "table", and the other five values from the return-value table. The cost of forgetting quotes is a runtime crash in an error path, which is exactly where you least want one.
nil vs nonexistent keys
Accessing a nonexistent table key returns nil, and type() on nil returns "nil":
t = {}
type(t.missing) -- "nil"
type(undefined_var) -- "nil" (even for global variables that don't exist)
This means you cannot use type() alone to distinguish between a missing key and a key explicitly set to nil — both produce "nil". The ambiguity is inherent to Lua’s table model: unset keys and nil-set keys are indistinguishable at the value level. To check for key existence regardless of the stored value, use rawget() for a single key or iterate with next(). The code below demonstrates the difference: type(t.k) returns "nil" even though the key "k" was explicitly assigned nil.
t = {k = nil}
type(t.k) -- "nil"
rawget(t, "k") -- nil (but key exists)
next(t) -- "k", nil (key exists)
type() with Metatables
The key-presence check is useful for table introspection, but it does not change what type() reports about the table itself. Metatables add another layer: they can override operators, define __index fallbacks, and change how a table behaves, but they do not change what type() returns. A table with a metatable is still "table", not a custom type name. This is by design — type() reflects the C-level type tag, not the metatable’s metadata.
mt = {}
mt.__index = mt
t = setmetatable({}, mt)
type(t) -- "table" (not "custom")
getmetatable(t) -- the metatable
To implement custom types, store the type name in the table itself or in the metatable and check it separately. Many Lua OOP libraries follow this pattern — they set a __index on a metatable pointing to a prototype table, and you identify instances by comparing getmetatable(obj) against the class metatable. The Point example below shows the convention: type(p) still returns "table", but getmetatable(p) == Point confirms that p was created by the Point.new constructor.
Point = {}
Point.__index = Point
function Point.new(x, y)
return setmetatable({x = x, y = y}, Point)
end
p = Point.new(2, 5)
type(p) -- "table"
getmetatable(p) == Point -- true (check via metatable)
type() vs inspect
The metatable-identity pattern works for OOP-style type checking, but for ad-hoc debugging you often want more detail than type() provides. type() tells you the broad category — number, table, function — but not what the value contains or how it is structured. When inspecting tables, you typically want to know whether a metatable is attached, how many keys the table holds, and whether it behaves like an array or a dictionary. The helper below extends type() with metatable awareness for richer debug output.
-- type() gives you the category
type(42) -- "number"
-- For tables, you often want more detail
function describe_table(t)
local mt = getmetatable(t)
if mt and mt.__type then
return mt.__type
end
return "table"
end
Common Patterns
The describe_table helper extends type() with metatable inspection, but most real-world uses of type() fall into two simple patterns. The first is strict validation — check the type and error early if it is wrong, before any computation happens. The second is type-based dispatch — route the value to the right handler based on what kind of data it is. Both patterns appear constantly in idiomatic Lua code.
function init(config)
if type(config) ~= "table" then
error("config must be a table")
end
if type(config.host) ~= "string" then
error("config.host must be a string")
end
if type(config.port) ~= "number" then
error("config.port must be a number")
end
-- proceed with config.host, config.port
end
Dispatch on type:
The config-validation pattern checks types and raises errors — a strict gate. The dispatch pattern below takes the opposite approach: it uses type() to route a value through the correct conversion path, accepting any input and producing the best possible output. Both patterns are common in real Lua code, and which one you choose depends on whether you want your function to be strict about its contract or lenient about the data it accepts.
function tostring_safe(v)
if type(v) == "string" then return v
elseif type(v) == "number" then return tostring(v)
elseif type(v) == "boolean" then return tostring(v)
elseif type(v) == "table" then return tostring(v)
else return tostring(v) end
end
See Also
- /reference/core-functions/ref-print/ — output values to the console
- /reference/core-functions/ref-tostring/ — convert any Lua value to a string
- /guides/lua-metatables/ — custom types with metatables and __index