v0.13.8

Interfaces and enums

main · 4b0211c · latest release v0.13.8source on GitHub ↗

An interface describes the shape of a plain table, such as saved settings or a roster entry, so that every field you read or write is checked. An enum names a fixed set of values. Interfaces produce no code, and enum members usually compile to plain numbers or strings.

Describe the shape of a table

An interface lists fields and their types. A ? after the name makes a field optional: it may be nil, and wowlua makes you check it before you use it as a value.

src/roster.wowluawowlua
export interface RaidMember    name: string    class: string    role: "TANK" | "HEALER" | "DAMAGER"    note?: string                  -- may be nil    readonly joinedAt: numberend export function describe(m: RaidMember): string    local text = `{m.name} ({m.role})`    if m.note then        text ..= `: {m.note}`      -- a string here, not nil    end    return textend
  • role takes only the three strings listed. A misspelled "HEAL" is an error, and so is a field the interface doesn't declare.
  • A readonly field is set when the table is created. Assigning it later is error E2010.

Any table with the right fields fits

A table never has to say that it is a RaidMember. If it has the fields with the right types, it fits: a table literal, a table from SavedVariables, or an instance of a class. Here the values come from GetRaidRosterInfo, whose return types are part of the typed WoW API:

src/sync.wowluawowlua
import RaidMember, describe from "./roster" local function toRole(raw: string?): "TANK" | "HEALER" | "DAMAGER"    if raw == "TANK" or raw == "HEALER" then        return raw    end    return "DAMAGER"end local roster: RaidMember[] = {} for i = 1, GetNumGroupMembers() do    local name, _, _, _, _, class, _, _, _, _, _, role = GetRaidRosterInfo(i)    roster[#roster + 1] = {        name = name,        class = class,        role = toRole(role),        joinedAt = time(),    }end for _, member in ipairs(roster) do    print(describe(member))end

GetRaidRosterInfo returns the role as a string, which could be any text, so toRole turns it into one of the three values. Comparing raw with a string narrows it to that string, so return raw fits the return type.

A table may carry more fields than the interface lists.

Interfaces with methods

An interface can list methods as well, written like a method of a class without a body. Then different objects can be handled the same way. Anything that reports a number can be shown here:

src/display.wowluawowlua
export interface Readout    title: string    function current(): numberend class GoldReadout implements Readout    title: string = "Gold"     function current(): number        return math.floor(GetMoney() / 10000)    endend class BagReadout implements Readout    title: string = "Free slots"     function current(): number        local free = 0        for bag = 0, 4 do            free += C_Container.GetContainerNumFreeSlots(bag)        end        return free    endend local function render(items: Readout[]): string    local parts: string[] = {}    for _, r in ipairs(items) do        parts[#parts + 1] = `{r.title}: {r:current()}`    end    return table.concat(parts, "  ")end print(render({ GoldReadout.new(), BagReadout.new() }))

implements is optional: both classes fit Readout without it, because a class instance fits an interface through its public fields, accessors and methods. What implements adds is the check where the class is declared: a class that misses a member, or has one of the wrong type, is error E3006 there, not only where it is used as the interface, as in render above.

wowlua
interface Readout    title: string    function current(): numberend class EmptyReadout implements Readout    title: string = "Nothing"end print(EmptyReadout)

Extend and combine interfaces

An interface can extend one or more other interfaces, and it can take type parameters like a generic class.

src/loot.wowluawowlua
interface Named    name: stringend interface Ordered<T>    function compare(other: T): numberend export interface LootEntry extends Named, Ordered<LootEntry>    itemLevel: number    winner?: stringend export function sortLoot(entries: LootEntry[])    table.sort(entries, function(a, b)        return a:compare(b) < 0    end)end

A LootEntry has name, compare, itemLevel and winner.

Tables of several kinds

When a value can be one of several shapes, give each shape a field with a fixed string and combine them in a union. Checking that field tells wowlua which shape you have. The typed demo addon records money coming in and going out this way:

src/money.wowluawowlua
export interface Earned    kind: "earned"    time: number    amount: number    source: stringend export interface Spent    kind: "spent"    time: number    amount: numberend export type Change = Earned | Spent export function isEarned(change: Change): change is Earned    return change.kind == "earned"end local history: Change[] = {    { kind = "earned", time = time(), amount = 5000, source = "Loot" },    { kind = "spent", time = time(), amount = 1200 },} for _, change in ipairs(history) do    if isEarned(change) then        print(`+{change.amount} from {change.source}`)   -- only Earned has source    else        print(`-{change.amount}`)    endend

The return type change is Earned makes isEarned a type predicate: where the call is true, change is an Earned, and in the else branch a Spent. Comparing change.kind == "spent" directly narrows in the same way. Reading change.source on a Spent is error E2003. Types covers narrowing in full.

Type the addon table

Every file of an addon receives the same private table as the second value of .... Describe what you store in it with declare interface AddonNamespace in a declaration file. Declarations of AddonNamespace in several files are merged.

src/types.d.wowluawowlua
declare interface Settings    scale: number    locked: booleanend declare interface AddonNamespace    db: Settingsend
src/core.lualua
local addonName, ns = ...ns.db.scale = 2          -- checked against Settingsns.lastSync = time()     -- not declared: allowed, typed any

Fields you haven't declared stay allowed, so an existing addon keeps building while you add types one field at a time. In .wowlua files the declared fields are checked. .lua files are type-checked only when the manifest sets "check": { "lua": true }, and then their problems are warnings.

Enums

An enum names a fixed set of values, such as item qualities, display modes or anchor points. Members without a value are numbered from 1, each one more than the member before it. Give a member a value to start elsewhere. String enums suit values that go into saved data or into the game's API.

src/enums.wowluawowlua
export enum BarMode { Hidden, Combat, Always }            -- 1, 2, 3 export enum Quality { Poor = 0, Common, Uncommon, Rare, Epic, Legendary }   -- 0 to 5 export enum Anchor {    TopLeft = "TOPLEFT",    Center = "CENTER",} export function shouldShow(mode: BarMode): boolean    if mode == BarMode.Always then        return true    end    return mode == BarMode.Combat and InCombatLockdown()end local frame = CreateFrame("Frame", nil, UIParent)frame:SetPoint(Anchor.Center)     -- an Anchor is a string, so SetPoint takes it

A BarMode member fits wherever a number is expected, but a plain number is not a BarMode:

wowlua
shouldShow(7)
error[E2001]: `7` is not assignable to `BarMode`
   ┌─ src/enums.wowlua:19:12
   │
19 │ shouldShow(7)
   │            ^ here
  • An enum is all numbers or all strings (E3010), and no two members have the same value (E3011).
  • Enums are declared at the top level of a file (E0115).
  • Members are separated by , or ;, and a trailing separator is allowed.
  • Assigning to a member, as in BarMode.Hidden = 5, is error E2010.

What enums compile to

A member read with a constant name is replaced by its value. When the enum is only read that way, no table is created. The palette of the widget demo addon works like this:

wowlua
enum Palette {    Accent = "33ff99",    Muted = "9d9d9d",} local title = UIParent:CreateFontString(nil, "OVERLAY", "GameFontNormal")title:SetText(`|cff{Palette.Accent}wowlua|r`)
lua
local title = UIParent:CreateFontString(nil, "OVERLAY", "GameFontNormal")title:SetText("|cff33ff99wowlua|r")

An enum used as a value, such as one that is exported, passed to a function or iterated, gets a table that holds exactly its members. The BarMode above compiles to:

lua
BarMode = {Hidden = 1, Combat = 2, Always = 3}__wl.reflectEnum(BarMode, {"Hidden", "Combat", "Always"})

__wl.reflectEnum records the declaration order for wowlua:reflect, outside the table. In debug builds the table also gets a metatable that raises an error when code reads a member that doesn't exist or assigns a new key, instead of quietly giving nil. pairs sees the same members in every build.

Convert saved values to enums

A value read from SavedVariables or a chat message is a plain number or string. wowlua:reflect converts it with a check, and gives you member names for display:

src/options.wowluawowlua
import * as reflect from "wowlua:reflect"import BarMode from "./enums" local db = { barMode = 2 :: unknown } local mode = reflect.enumFrom(BarMode, db.barMode) ?? BarMode.Combatprint(`bars: {reflect.enumName(BarMode, mode)}`)        -- bars: Combat for _, entry in ipairs(reflect.enumEntries(BarMode)) do    print(entry.name, entry.value)                      -- in declaration orderend

enumFrom returns nil for a value that isn't a member, so an old or hand-edited saved value falls back to your default. enumName returns the member's name, and enumEntries the members as { name, value } records in declaration order. Debugging helpers covers the rest of wowlua:reflect.

The full rules are in Classes, interfaces and enums and Types.

main · 4b0211csource on GitHub ↗