luaguides

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

Use type() in conditional statements to branch based on the type of a value:

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

type() is commonly used as a guard to validate function arguments:

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 “empty table” as a separate type. Use #table == 0 or next(table) == nil to check if a table is empty.

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, and will cause a attempt to compare string with boolean error.

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 can’t use type() alone to distinguish between a missing key and a key explicitly set to nil. Use rawget() or check for key presence:

t = {k = nil}
type(t.k)           -- "nil"
rawget(t, "k")      -- nil (but key exists)
next(t)             -- "k", nil (key exists)

type() with Metatables

type() returns the type of the object itself, not the value of the metatable’s __type field. Metatables do not change what type() returns:

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:

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

For debugging where type() gives you the name and you want more detail:

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

Validate configuration tables:

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:

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