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 builtWith 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:
- Lune decodes null as nil. It has no way to write null back out.
- Zune decodes null as a
JsonValue.Nulltable when you passpreserve_null. - Lute decodes null as
json.null, a sentinel made withnewproxy. - seal 0.8 used
json.null(), a special userdata value.
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
noneis falsy, like nil and false.none == noneis true.none == nilis false.type(none)is "none".- Storing none in a table keeps the key.
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)
endUse 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)
endThe phone field is listed as none. Code reading the report knows it was left empty.
Things to know
- Optional types read
T?for nil. WriteT | nonefor none. - Comparing none with
<or>is an error, like nil. - none is a global value. A local named
nonewould hide it.