Tables

From the type checker perspective, each table can be in one of three states. They are: unsealed table, sealed table, and generic table. This is intended to represent how the table’s type is allowed to change.

Unsealed tables

An unsealed table is a table which supports adding new properties, which updates the tables type. Unsealed tables are created using table literals. This is one way to accumulate knowledge of the shape of this table.

local t = {x = 1} -- {x: number}
t.y = 2           -- {x: number, y: number}
t.z = 3           -- {x: number, y: number, z: number}

However, if this local were written as local t: { x: number } = { x = 1 }, it ends up sealing the table, so the two assignments henceforth will not be ok.

Furthermore, once we exit the scope where this unsealed table was created in, we seal it.

local function vec2(x, y)
    local t = {}
    t.x = x
    t.y = y
    return t
end

local v2 = vec2(1, 2)
v2.z = 3 -- not ok

Unsealed tables are exact in that any property of the table must be named by the type. Since Luau treats missing properties as having value nil, this means that we can treat an unsealed table which does not mention a property as if it mentioned the property, as long as that property is optional.

local t = {x = 1}
local u : { x : number, y : number? } = t -- ok because y is optional
local v : { x : number, z : number } = t  -- not ok because z is not optional

Sealed tables

A sealed table is a table that is now locked down. This occurs when the table type is spelled out explicitly via a type annotation, or if it is returned from a function.

local t : { x: number } = {x = 1}
t.y = 2 -- not ok

Sealed tables are inexact in that the table may have properties which are not mentioned in the type. As a result, sealed tables support width subtyping, which allows a table with more properties to be used as a table with fewer properties.

type Point1D = { x : number }
type Point2D = { x : number, y : number }
local p : Point2D = { x = 5, y = 37 }
local q : Point1D = p -- ok because Point2D has more properties than Point1D

Generic tables

This typically occurs when the symbol does not have any annotated types or were not inferred anything concrete. In this case, when you index on a parameter, you’re requesting that there is a table with a matching interface.

local function f(t)
    return t.x + t.y
           --^   --^ {x: _, y: _}
end

f({x = 1, y = 2})        -- ok
f({x = 1, y = 2, z = 3}) -- ok
f({x = 1})               -- not ok

Table indexers

These are particularly useful for when your table is used similarly to an array.

local t = {"Hello", "world!"} -- {[number]: string}
print(table.concat(t, ", "))

Luau supports a concise declaration for array-like tables, {T} (for example, {string} is equivalent to {[number]: string}); the more explicit definition of an indexer is still useful when the key isn’t a number, or when the table has other fields like { [number]: string, n: number }.