Luau tips
Get past the Luau type checker traps that cost the most time, each with the exact error it prints and the fix.
These come from Luau's type checker (Studio's old solver), not from ShotScript, so plain Luau hits them too. The examples use these types:
public type Kind = "Tool" | "Food"
public type Def = { Id : string, Kind : Kind }
public type Slot = Def | false
local function useKind(k : Kind) end
local function useDef(d : Def) end1. String literals widen to string
A literal like "Tool" keeps its literal type only where the checker already expects Kind. Stored without a type, it becomes plain string, and string doesn't fit Kind. The error is Expected this to be '"Food" | "Tool"', but got 'string'; none of the union options are compatible.
Where it happens:
local kinds = { "Tool", "Food" }
useKind(kinds[1]) -- an array literal
local raw = { Kind = "Tool" }
useDef({ Id = "x", Kind = raw.Kind }) -- a field read from an untyped table
local DEFS = { { Id = "a", Kind = "Tool" } }
useDef(DEFS[1]) -- a table of records without a typeThe fix: give the local its type, so the literals are checked against it when they are written.
local kinds : {Kind} = { "Tool", "Food" }
useKind(kinds[1])
local raw : { Kind : Kind } = { Kind = "Tool" }
useDef({ Id = "x", Kind = raw.Kind })
local DEFS : {Def} = { { Id = "a", Kind = "Tool" } }
useDef(DEFS[1])Loop variables widen even when the array is typed. for _, k in all, for _, k in ipairs(all) and a loop over a literal array all give k the type string:
local all : {Kind} = { "Tool", "Food" }
for _, k in all do
useKind(k) -- Expected this to be '"Food" | "Tool"' but got 'string | string'
endAnnotate the loop variable instead (a typed local inside the loop doesn't help, k is already string):
for _, k : Kind in all do
useKind(k)
endA field of a record keeps its type (for _, d in defs do useKind(d.Kind) end is fine), and so does a return: a function declared : Kind may return if b then "Tool" else "Food".
2. X | false doesn't narrow
local s = slots[1] -- slots : {Slot}, Slot = Def | false
if s then
print(s.Id) -- Key 'Id' is missing from 'true' in the type 'Def | true'
endif s then, if s ~= false then and if not s then return end all leave a boolean in the type. Fixes:
if typeof(s) == "table" then
print(s.Id)
end
if s then
local d = s :: Def
print(d.Id)
end
local d : Def? = slots[1] or nil -- turn false into nil once; Def? narrows normally
if d then
print(d.Id)
endUse T? instead of T | false wherever the value isn't an array slot. (An array can't hold nil, which is why an array of slots marks an empty one with false.)
3. Indexing {[Union] : T} with a literal
local prices : {[Kind] : number} = { Tool = 1, Food = 2 }
print(prices.Tool) -- Key 'Tool' not found in table '{ ["Food" | "Tool"]: number }'
print(prices["Food"]) -- Expected this to be '"Food" | "Tool"', but got 'string'Fix: give the key its type.
print(prices["Tool" :: Kind])
local k : Kind = "Food"
print(prices[k])4. t[k] or {} makes a union
local counts : {[string] : {[string] : number}} = {}
local c = counts[key] or {}
c.wins = (c.wins or 0) + 1
-- Key 'wins' is missing from '{| |}' in the type '{ [string]: number } | {| |}'
local list = byKind[kind] or {} -- byKind : {[string] : {Def}}
for _, d in list do -- Cannot call a value of type {Def} in union: {Def} | {| |}
endThe empty {} has its own type, so the local is "the real table or an empty one". Fix: annotate the local.
local c : {[string] : number} = counts[key] or {}
local list : {Def} = byKind[kind] or {}5. Smaller ones
- An untyped parameter is
any: "Unknown type used in + operation; consider adding a type annotation to 'x'". Type every parameter. local self = init()isany, so nothing in the constructor is checked. Writelocal self : Shape = init().- Casting with
:: anyswitches checking off for that value; everything it touches goes unchecked.
