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 color

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

Things to know