The none Primitive

none is a value that means "intentionally empty".

It behaves like nil, except that you can store it in a table.

Why nil isn't enough

In Luau, setting a table field to nil deletes it. The field is gone, not empty.

In an array, nil leaves a hole that makes the length operator unreliable.

local list = { "first", nil, "third" }
print(#list) -- 3 or 1, depending on how the table was built

With none, the slot stays filled:

const list = { "first", none, "third" }
print(#list)
print(list[2] == none)

JSON shows the problem best: it has null, and Luau has no value that can stand for it.

Decode {"nickname": null} into a Luau table, and the nickname key is gone. Encode that table again, and the field is missing from the output. The data doesn't round-trip.

So every Luau runtime invents a different workaround:

Code written against one runtime's sentinel breaks on every other runtime. The sentinels are all truthy, too: if user.nickname then treats a null nickname as a real one.

In Luwu, JSON null is none. seal decodes null as none and encodes none as null. The data round-trips, and none is falsy like nil:

const json = require("@std/json")

const user = json.decode('{"name": "Ada", "nickname": null}')
print(user.nickname == none)
print(user.nickname or "no nickname")
print(json.raw(user))

How none behaves

const settings: { [string]: (string | none)? } = { theme = "dark", font = none }

print(settings.font == none)
print(settings.missing == nil)
print(settings.font or "default font")

for key, value in settings do
    print(key, value)
end

Use cases

none fits anywhere "no value" is real data.

A form might have a middle name field that's empty on purpose:

type Person = { first: string, middle: string | none, last: string }

const ada: Person = { first = "Ada", middle = none, last = "Lovelace" }

const function full_name(person: Person): string
    if person.middle ~= none then
        return `{person.first} {person.middle} {person.last}`
    end
    return `{person.first} {person.last}`
end

print(full_name(ada))

An array can mark items as left out without leaving holes:

const show_admin = false

const menu = {
    "Home",
    "Profile",
    if show_admin then "Admin" else none,
    "Log out",
}

for _, item in menu when item ~= none do
    print(item)
end
print(#menu)

Listing every field

class.fields gives you an object's public fields as a table.

Every field name is still there because a field that's nil shows up as none.

class Profile
    name = "Ada"
    nickname: string? = nil
    level = 12
end

const fields = class.fields(Profile())
print(fields.name, fields.nickname, fields.level)
print(fields.nickname == none)

This prints Ada, none, 12. With nil, the nickname would vanish from the table.

The checker knows it too: the nickname field's type is string | none.

Your own functions can work the same way. This one reports a form, empty fields included:

const function snapshot(values: { [string]: string? }, keys: { string }): { [string]: string | none }
    return { for _, key in keys give [key] = values[key] or none }
end

const form = snapshot({ email = "ada@example.com" }, { "email", "phone" })
for key, value in form do
    print(key, value)
end

The phone field is listed as none. Code reading the report knows it was left empty.

Things to know