Loading...
Loading...
Luau type system: annotations, generics, narrowing, inference philosophy, sealed/unsealed tables, exports, and Roblox-aware typing.
npx skill4agent add tabooharmony/roblox-brain roblox-luau-types--!strict--!nonstrict--!nocheckanytypeof()IsA()roblox-luau-coreroblox-luau-patternsroblox-*--!strictselfanyany::<T>anytypeof()IsA()::--!strict -- Full type checking. Errors on unresolved types. Use for new code.
--!nonstrict -- Default mode. Warns but allows unresolved types. Good for transitional code.
--!nocheck -- Disables type checking entirely. Only for generated code or legacy.--!nonstrict--!strict-- Variable annotations
local name: string = "Alice"
local health: number = 100
local isAlive: boolean = true
local data: any = nil -- opt out of type checking
-- Function parameter and return types
local function add(a: number, b: number): number
return a + b
end
-- Optional parameters
local function greet(name: string, title: string?): string
if title then
return `{title} {name}`
end
return name
end-- Array type
local scores: { number } = { 100, 95, 87 }
-- Dictionary type (indexer)
local config: { [string]: boolean } = {
shadows = true,
particles = false,
}
-- Record type (concrete fields)
type PlayerData = {
name: string,
level: number,
inventory: { string },
stats: {
health: number,
mana: number,
},
}
local player: PlayerData = {
name = "Alice",
level = 10,
inventory = { "sword", "shield" },
stats = {
health = 100,
mana = 50,
},
}-- UNSEALED: unannotated local tables accumulate fields
local config = {}
config.debug = true -- fine, table is unsealed
config.version = "1.0" -- fine, still accumulating
-- SEALED: once annotated or returned, no new fields allowed
local settings: { debug: boolean } = { debug = true }
settings.version = "1.0" -- ERROR: 'version' not in type
-- Practical implication: build tables fully before annotating
local data = {
name = "Alice",
level = 10,
}
-- data is unsealed here, you can still add fields
data.guild = "Warriors"
-- But once you pass it to a typed function or return it, it seals-- Union type: value can be one of several types
local id: string | number = "abc123"
id = 42 -- also valid
-- Optional is shorthand for T | nil
local nickname: string? = nil -- equivalent to string | nil
-- Useful for function returns that may fail
local function findPlayer(name: string): Player?
-- ...
return nil
end
-- Tagged unions for state machines (discriminated unions)
type Loading = { kind: "loading" }
type Ready<T> = { kind: "ready", value: T }
type Failed = { kind: "failed", message: string }
type State<T> = Loading | Ready<T> | Failed
local function readValue(state: State<number>): number?
if state.kind == "ready" then
return state.value -- narrowed to Ready<number>
end
return nil
end-- typeof narrows types (Roblox-aware, preferred over type())
local function process(value: string | number)
if typeof(value) == "string" then
-- value is narrowed to string here
print(string.upper(value))
else
-- value is narrowed to number here
print(value * 2)
end
end
-- Instance type checking with :IsA()
local function handlePart(instance: Instance)
if instance:IsA("BasePart") then
-- instance is narrowed to BasePart
instance.Anchored = true
instance.BrickColor = BrickColor.new("Bright red")
end
end
-- assert for non-nil narrowing
local function getPlayerData(player: Player): PlayerData
local leaderstats = player:FindFirstChild("leaderstats")
assert(leaderstats, "Player missing leaderstats")
-- leaderstats is now narrowed to non-nil
return parseStats(leaderstats)
end
-- Pattern: type guard function
local function isWeapon(item: Item): boolean
return item.category == "weapon"
end-- Generic function: preserves element type through transforms
local function first<T>(list: { T }): T?
return list[1]
end
local name = first({ "Alice", "Bob" }) -- inferred as string?
local num = first({ 1, 2, 3 }) -- inferred as number?
-- Generic type alias
type Result<T> = {
success: boolean,
value: T?,
error: string?,
}
local function fetchData(): Result<PlayerData>
return {
success = true,
value = { name = "Alice", level = 10, inventory = {}, stats = { health = 100, mana = 50 } },
error = nil,
}
end
-- Generic class-like pattern
type Stack<T> = {
items: { T },
push: (self: Stack<T>, value: T) -> (),
pop: (self: Stack<T>) -> T?,
peek: (self: Stack<T>) -> T?,
}
-- NOTE: In type definitions, self is explicit (it's a function signature).
-- In actual method definitions, use : to hide self (see roblox-luau-patterns).<any>-- In a ModuleScript, export types for other modules to use
-- File: ReplicatedStorage/Types.lua
export type WeaponData = {
name: string,
damage: number,
rarity: "Common" | "Rare" | "Epic" | "Legendary",
durability: number,
}
export type InventorySlot = {
item: WeaponData?,
quantity: number,
}
-- Consumers import with require
-- File: ServerScriptService/WeaponService.lua
local Types = require(game.ReplicatedStorage.Types)
local function createWeapon(name: string, damage: number): Types.WeaponData
return {
name = name,
damage = damage,
rarity = "Common",
durability = 100,
}
end-- Instance hierarchy types
local part: Part = Instance.new("Part")
local model: Model = Instance.new("Model")
local player: Player = game.Players.LocalPlayer
local character: Model = player.Character or player.CharacterAdded:Wait()
local humanoid: Humanoid = character:FindFirstChildWhichIsA("Humanoid") :: Humanoid
-- Value types (these are NOT instances - they are value types / structs)
local position: Vector3 = Vector3.new(10, 5, 0)
local rotation: CFrame = CFrame.new(0, 10, 0) * CFrame.Angles(0, math.rad(90), 0)
local color: Color3 = Color3.fromRGB(255, 0, 0)
local size: Vector2 = Vector2.new(100, 50)
local region: Region3 = Region3.new(Vector3.new(-10, 0, -10), Vector3.new(10, 20, 10))
local ray: Ray = Ray.new(Vector3.new(0, 10, 0), Vector3.new(0, -1, 0))
local udim2: UDim2 = UDim2.new(0.5, 0, 0.5, 0)
-- Enum types
local material: Enum.Material = Enum.Material.Grass
local partType: Enum.PartType = Enum.PartType.Ball--!strict
-- Derive instance type from setmetatable for precise self typing
local Counter = {}
Counter.__index = Counter
type CounterData = { value: number }
export type Counter = typeof(setmetatable({} :: CounterData, Counter))
function Counter.new(initialValue: number): Counter
return setmetatable({ value = initialValue }, Counter)
end
-- Explicit self annotation when : syntax doesn't infer precisely enough
function Counter.increment(self: Counter, amount: number): number
self.value += amount
return self.value
end
return Counterselfself:self.:--!nonstrictanyany:self::string & number--!strictany::