Literal Types and Validators
Some strings follow rules: hex colors, CSS sizes, routes, ids.
Luwu can check those rules while you type and report a wrong string as a type error with your own message.
Literal types
A literal is a value written straight into the code, like "red". A type can be a single literal:
type Direction = "north" | "south" | "east" | "west"
const function walk(direction: Direction)
print(`Walking {direction}`)
end
walk("north")Calling walk("up") is a type error in both Luau and Luwu.
Luwu adds literal<T>: it asks for a literal of type T and uses the exact value as the type.
const function tag(name: literal<string>): string
return `<{name}>`
end
print(tag("main"))Passing a string variable to tag is a type error: the argument has to be written out.
Validators
A validator is a type function that checks a literal while the code is checked, not at runtime. It gets the literal's type and returns that type when the value is fine, or types.error with a message when it isn't.
type function HexColor(value: type): type
if not value:is("singleton") then
return types.error("expected a literal like \"#3b5bdb\"")
end
const text = value:value()
if typeof(text) ~= "string" or string.sub(text, 1, 1) ~= "#" then
return types.error(`'{tostring(text)}' is missing its '#'`)
end
const digits = string.sub(text, 2)
if string.find(digits, "[^%x]") or (#digits ~= 3 and #digits ~= 6) then
return types.error(`'{text}' isn't a hex color`)
end
return value
end
const function paint(color: HexColor<literal<string>>)
print(`Painting with {color}`)
end
paint("#3b5bdb")
-- paint("3b5bdb") is a type error: '3b5bdb' is missing its '#'
-- paint("#12345") is a type error: '#12345' isn't a hex colorThe error shows up in your editor at the call.
Naming a validated type
Give the validated type a name to use it in many places:
type function Percent(value: type): type
if not value:is("singleton") then
return types.error("expected a literal like \"50%\"")
end
const text = value:value()
const number = typeof(text) == "string" and tonumber(string.match(text, "^(%d+)%%$"))
if not number or number > 100 then
return types.error(`'{tostring(text)}' isn't a percentage from 0% to 100%`)
end
return value
end
type Percentage = Percent<literal<string>>
const function set_volume(volume: Percentage)
print(`Volume: {volume}`)
end
const function set_brightness(brightness: Percentage)
print(`Brightness: {brightness}`)
end
set_volume("80%")
set_brightness("100%")In tables and unions
Validated types work on table fields too: each field is checked where it's written.
A union can mix one validated literal with other types, like numbers:
type function Size(value: type): type
if not value:is("singleton") then
return types.error("expected a literal like \"12px\"")
end
const text = value:value()
if typeof(text) ~= "string" or not string.match(text, "^%d+px$") then
return types.error(`'{tostring(text)}' isn't a pixel size`)
end
return value
end
type Pixels = Size<literal<string>> | number
type ButtonStyle = {
width: Pixels?,
height: Pixels?,
}
const function button(label: string, style: ButtonStyle)
print(label, style.width, style.height)
end
button("Play", { width = "120px", height = 40 })
-- button("Quit", { width = "120" }) is a type error: '120' isn't a pixel sizeThings to know
- Validators only see literals. A string built at runtime can't be checked this way.
- Inside the function, the parameter is a plain
string. - Type functions are written in Luwu and use the
typeslibrary to inspect types.