Id kinds

Declare a kind of string id, like id ProductId, and shot check catches every misspelt id in the game and names the one you meant.

An id kind is one line in a system's Types.shot. That system's own code defines the ids; everywhere else, a literal of that kind must be one of them.

What it is

An id is a string that names one of a fixed set of things: a product, an item, an enemy. A line

id ProductId

in a system's Types.shot declares the kind. To Luau a ProductId is a string (the Types module gets export type ProductId = string), so it costs nothing at run time and every string function takes it. What it adds is a check: every string literal that reaches a ProductId slot must be one of the ids the owning system defines.

The whole syntax

Market/Types.shot

doc { A product the shop sells, like "golden_hat" }
id ProductId

public type Offer = { Product : ProductId, Price : number }
  • id Name goes on its own line in a Types.shot, with an optional doc above it. It is public: other systems import it like a type (import ProductId from @shot.Market.Catalog, or from Market when Market has an entry point).
  • Then write : ProductId wherever an id of that kind goes: parameters, fields of types, locals, returns, lists ({ProductId}) and map keys ({[ProductId] : number}).

Which ids exist

The system that declares the kind defines its ids: every ProductId literal in its folder (subfolders included) is an id. Anywhere else a ProductId literal must be one of them. Nothing else declares ids, so there is no list to keep.

Market/Catalog.shot

types script.Parent.Types

local offers : {Offer} = {}

local function sell(product : ProductId, price : number)
    table.insert(offers, { Product = product, Price = price })
end

rootdoc {
    The products the shop sells and their prices.
}
module Catalog = {
    doc { Fills the catalog }
    function Load()
        sell("golden_hat", 100)
        sell("speed_boots", 250)
    end

    doc { The price of a product, or nil }
    function PriceOf(product : ProductId) : number?
        for _, offer in offers do
            if offer.Product == product then
                return offer.Price
            end
        end
        return nil
    end
}

"golden_hat" and "speed_boots" reach the product parameter of sell, so they are the ProductIds.

Prizes/Types.shot

import ProductId from @shot.Market.Catalog

public type Prize = { Product : ProductId, Count : number }

Prizes/Prizes.shot

types script.Parent.Types

use @shot.Market.Catalog

local daily : {Prize} = {
    { Product = "golden_hat", Count = 1 },
    { Product = "speed_boots", Count = 2 },
}

rootdoc {
    The daily prizes.
}
module Prizes = {
    doc { What the daily prizes cost together }
    function Total() : number
        local total = 0
        for _, prize in daily do
            total += (Catalog.PriceOf(prize.Product) or 0) * prize.Count
        end
        return total
    end
}

Write "speed_boot" instead of "speed_boots" in Prizes and shot check says:

Prizes/Prizes.shot:7:14   "speed_boot" is not a ProductId (nearest: "speed_boots"): the ids are the ProductId literals in Market; shot ids ProductId lists them

Where a literal is checked

  • An argument of a function, method or constructor whose parameter is a ProductId: Catalog.PriceOf("golden_hat"), sell("golden_hat", 100), Shop.new("golden_hat"), shop:Stock("golden_hat"), local functions included, and a typed event's Publish (see Events): PrizeGiven.Publish(player, "golden_hat").
  • A field of a table whose type is known: { Product = "golden_hat", Count = 1 } passed where a Prize goes, put in a local or a module field typed Prize or {Prize} (Daily : {Prize} = { ... }), returned from a function that returns one, or nested in such a table.
  • An element of {ProductId}, and a key of {[ProductId] : number} (written ["golden_hat"] = 1 or golden_hat = 1).
  • local p : ProductId = "golden_hat", return "golden_hat" in a function that returns ProductId, and "golden_hat" :: ProductId.

A call is matched to what it calls by the receiver: a use or a class name (Catalog.PriceOf), module and self, a local made by a constructor (local shop = Shop.new()), a chain (Shop.new("x"):Stock("y")). When the receiver can't be told, every function of that name in the project must give the same parameter type, or the literal isn't checked.

What it doesn't check

  • Literals inside the owning system are never errors: they define ids. A typo there makes a new id. shot ids ProductId lists every id with the places that define it and how often other systems use it, so an id written once and used nowhere stands out.
  • A literal that reaches the slot through something untyped: local name = "golden_hat" and then sell(name, 1), a table built without a known type, a field set afterwards (prize.Product = "x"), a value from a string function.
  • Comparisons and lookups: if product == "golden_hat", prices["golden_hat"].
  • An id passed where a plain string is expected, or a plain string variable passed as a ProductId: to Luau both are strings, so neither is an error.

Listing them

shot ids               every id kind, with how many ids it has, its literals elsewhere and how many are no id
shot ids ProductId       the kind's slots, every id with where the owner defines it and how often others write
                         it, then every use outside the owner with its file:line and slot (NOT AN ID marks one
                         that isn't an id, with the nearest)

With the "speed_boot" typo above, shot ids ProductId prints:

id ProductId (Market/Types.shot:2): a string; the ids are the ProductId literals in Market, and a literal elsewhere must be one of them
slots with literals: Prize.Product, sell(product)

2 ids (where Market defines it; literals elsewhere):
  golden_hat    Market/Catalog.shot:15   1 literal
  speed_boots   Market/Catalog.shot:16   no literal elsewhere

2 uses outside Market, 1 not one of the ids:
  Prizes/Prizes.shot:6   "golden_hat"   Prize.Product
  Prizes/Prizes.shot:7   "speed_boot"   Prize.Product   NOT AN ID (nearest: "speed_boots")

Errors

"speed_boot" is not a ProductId (nearest: "speed_boots"): the ids are the ProductId literals in Market; shot ids ProductId lists them
id GiftId goes in a system's Types.shot, the system whose code defines the ids
id ProductId is declared twice: here and in Market/Types.shot:2 (one system owns a kind)