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 ProductIdin 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 Namegoes 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
: ProductIdwherever 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 themWhere 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"] = 1orgolden_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 ProductIdlists 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 thensell(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)