
Luau Types
- 49 installs
- 10 repo stars
- Updated May 27, 2026
- stackfox-labs/luau-skills
Helps with ai & agent building tasks.
About
luau-types is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- luau-types
- AI & Agent Building
- AI-coding skill
Luau Types by the numbers
- 49 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #7,391 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/stackfox-labs/luau-skills --skill luau-typesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 49 |
|---|---|
| repo stars | ★ 10 |
| Last updated | May 27, 2026 |
| Repository | stackfox-labs/luau-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
luau-types
When to Use
Use this skill when the task is primarily about Luau's type system:
- Choosing between
--!strict,--!nonstrict, and other file type-checking modes. - Adding or correcting type annotations on variables, functions, tables, and modules.
- Designing APIs that preserve inference instead of collapsing to
any. - Modeling data with generics, unions, intersections, optionals, and tagged unions.
- Typing object-like tables, metatable-backed modules, and exported module surfaces.
- Writing or reviewing type functions and other advanced type-level utilities.
- Using Roblox class, datatype, enum, or
IsAknowledge only to improve static typing.
Do not use this skill when the task is mainly about:
- General Luau syntax, control flow, metatables, or standard-library usage outside typing concerns.
- Runtime performance, profiling, hot-path tuning, or allocation strategy.
- Roblox networking, replication, data storage, cloud APIs, or gameplay architecture.
Decision Rules
- Use this skill if the core question is "what should this type be?" or "how should this code type-check over time?"
- Prefer
--!strictguidance for new or actively maintained code unless the task explicitly targets transitional or legacy code. - Prefer inference-preserving designs over annotation-heavy designs when the inferred shape stays precise and readable.
- Prefer explicit exported aliases at module boundaries when consumers should share a stable contract.
- Use generics when the relationship between inputs and outputs matters; do not replace that relationship with
any. - Use tagged unions plus refinements when a value can be one of several structured cases.
- If the task shifts into pure language syntax, hand off to
luau-core. - If the task shifts into optimization or runtime cost tradeoffs, hand off to
luau-performance. - If the task requires Roblox runtime architecture beyond type names and type refinement, use the appropriate
roblox/*skill instead. - If unsure, exclude anything that is not directly needed to improve typing correctness, maintainability, or analyzer behavior.
Instructions
1. Start by identifying the file mode expectation:
--!strictfor strong inference and early error detection.--!nonstrictfor transitional code where unresolved values would otherwise become noisy.--!nocheckonly when the task explicitly requires disabled analysis.
2. Preserve useful inference before adding annotations everywhere. Add annotations where they clarify intent, stabilize module contracts, constrain self, or prevent unwanted widening to any. 3. Prefer concrete aliases for shared shapes:
- records for structured data,
- indexers for dictionaries,
{T}for arrays,- exported aliases for module-facing contracts.
4. Use optionals, unions, and intersections deliberately:
T?forT | nil,- tagged unions for state machines or result-like values,
- intersections to combine compatible table capabilities or function signatures.
5. Treat casts with :: as a precision tool, not a bypass. Use them to narrow overly generic inference, not to hide unrelated-type errors. 6. Design generics around relationships:
- preserve element type through transforms,
- carry key/value relationships through containers,
- avoid defaulting to
anywhen a type parameter can express intent.
7. Model tables according to how Luau analyzes them:
- unsealed tables can accumulate fields locally,
- annotated or returned tables become sealed,
- width subtyping applies to sealed records.
8. For object-like modules, separate instance data from class behavior, derive the instance type from setmetatable, and annotate self explicitly when methods need the shared class type. 9. Keep module API surfaces type-safe:
- export named aliases for consumer-facing data,
- keep implementation details internal,
- choose signatures that infer caller types cleanly.
10. Use Roblox type knowledge only for annotations and refinements, such as Instance, Part, Enum.Material, datatypes, and IsA-driven narrowing.
Using References
- Open
references/type-system-overview.mdfor file modes, structural typing, annotations, casts, and module-boundary guidance. - Open
references/basic-types-and-table-typing.mdfor builtin types, special types likeanyandunknown, function signatures, table states, and indexers. - Open
references/generics.mdfor generic aliases, generic functions, defaults on aliases, and inference-preserving container patterns. - Open
references/unions-and-intersections.mdfor result shapes, tagged unions, discriminants, and safe intersection usage. - Open
references/refinements.mdfor truthy checks,type(...)guards, equality narrowing, compound conditions, andassert-based narrowing. - Open
references/object-oriented-typing.mdfor metatable-backed class typing,selfannotations, constructor return types, and exported instance aliases. - Open
references/type-functions.mdfor analysis-time type computation, available libraries, and when advanced type-level transforms are justified. - Open
references/roblox-types-in-luau.mdfor Roblox class, datatype, enum, constructor, service, andIsAtyping behavior. - Do not open other skill references unless the request clearly crosses skill boundaries.
Checklist
- The chosen type-checking mode matches the maintenance goal of the file.
- Public module contracts are explicit where reuse matters.
- Inference is preserved where it remains precise.
anyis avoided unless intentionally opting out.- Tables are typed according to their actual shape and sealing behavior.
- Unions, intersections, and optionals reflect real states instead of vague catch-all types.
- Generic parameters encode input/output relationships that callers rely on.
- Method
selftyping is explicit where Luau cannot safely infer the shared class type. - Roblox types are used only to improve typing, not to drift into unrelated Roblox architecture.
- No general syntax tutorial, performance advice, or networking/data/cloud guidance is included.
Common Mistakes
- Leaving a variable unannotated in
--!nonstrictand unintentionally turning it intoany. - Replacing a useful generic relationship with
anyor an overly broad union. - Sealing a table too early with an annotation, then expecting to add fields later.
- Expecting method definitions with
:to automatically share a preciseselftype across the whole class. - Using
::to force unrelated conversions instead of fixing the underlying type design. - Building unions without a discriminant, then making downstream refinement difficult.
- Using intersections between incompatible primitives such as
string & number. - Mixing runtime Roblox architecture guidance into a type-only task.
Examples
Export a stable module contract
--!strict
export type User = {
id: number,
name: string,
nickname: string?,
}
local M = {}
function M.makeUser(id: number, name: string): User
return {
id = id,
name = name,
nickname = nil,
}
end
return MPreserve relationships with a generic function
--!strict
local function first<T>(items: {T}): T?
return items[1]
end
local a = first({1, 2, 3}) -- number?
local b = first({"x", "y"}) -- string?Refine a tagged union
--!strict
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
end
return nil
endType an object-like module with explicit self
--!strict
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
function Counter.increment(self: Counter, amount: number): number
self.value += amount
return self.value
end
return CounterBasic Types and Table Typing
Key Concepts
- Builtin primitives include
nil,boolean,number, andstring. - Special types matter for API design:
unknownrequires narrowing before use.neverrepresents impossible values.anyopts out of type safety.- Function types use
(input) -> outputsyntax. - Variadics use
...T; function type packs use...Tor tuple-like returns. - Tables are analyzed as unsealed, sealed, or generic depending on how they are created and used.
Rules
- Prefer precise concrete types over
any. - Use
unknownwhen a value exists but must be validated before use. - Use
T?for optional values instead of ad hoc unions withnil. - Use
{T}for arrays and{[K]: V}for dictionaries. - Expect returned or explicitly annotated tables to be sealed.
- Use explicit named record types for stable object shapes.
Patterns
Arrays and dictionaries
--!strict
local names: {string} = { "Ada", "Lin" }
local scores: {[string]: number} = {
Ada = 10,
Lin = 12,
}Optional fields in records
--!strict
type User = {
id: number,
nickname: string?,
}Seal a returned table intentionally
--!strict
local function makePoint(x: number, y: number): { x: number, y: number }
return { x = x, y = y }
endExamples
Use unknown when validation is required
--!strict
local function readNumber(value: unknown): number?
if type(value) == "number" then
return value
end
return nil
endAllow width subtyping with sealed records
--!strict
type Point1D = { x: number }
type Point2D = { x: number, y: number }
local point2: Point2D = { x = 1, y = 2 }
local point1: Point1D = point2Generics
Key Concepts
- Generics preserve relationships between values and types.
- Generic aliases parameterize reusable shapes such as pairs, lists, maps, and wrappers.
- Generic functions let Luau infer caller-specific types from arguments.
- Alias generics can have defaults; function generics cannot.
Rules
- Use a generic when the output type depends on the input type.
- Prefer a type parameter over
anywhen callers should retain specific types. - Name type parameters clearly when more than one role exists, such as
KandV. - Add explicit generic annotations only when inference is insufficient or readability improves.
- Do not assign default generic parameters to functions.
Patterns
Generic aliases
--!strict
type Box<T> = {
value: T,
}
type Dict<K, V> = {[K]: V}Generic functions that preserve element type
--!strict
local function identity<T>(value: T): T
return value
endGeneric modules with exported contracts
--!strict
export type Result<T, E> =
{ kind: "ok", value: T } |
{ kind: "err", error: E }Examples
Preserve input and output correspondence
--!strict
local function last<T>(items: {T}): T?
return items[#items]
endUse defaulted alias generics where sensible
--!strict
type Pair<T = string> = {
first: T,
second: T,
}
local words: Pair = {
first = "a",
second = "b",
}Object-Oriented Typing
Key Concepts
- Luau can type object-like modules built from tables and metatables, but
selfoften needs explicit help. - Separate instance data from class behavior.
- Derive the instance type from
setmetatablewithtypeof(...)or the relevant type function form. - Export the instance type when other modules consume it.
Rules
- Define a data shape type first for instance fields.
- Derive and export the instance type from
setmetatable. - Annotate constructor returns with the instance type when compatibility matters.
- Annotate
selfexplicitly on methods that operate on the shared class type. - Keep class and instance typing aligned so method assumptions match constructor output.
Patterns
Separate data from behavior
--!strict
local Account = {}
Account.__index = Account
type AccountData = {
name: string,
balance: number,
}
export type Account = typeof(setmetatable({} :: AccountData, Account))Constructor with explicit return type
--!strict
function Account.new(name: string, balance: number): Account
return setmetatable({
name = name,
balance = balance,
}, Account)
endMethod with explicit self
--!strict
function Account.deposit(self: Account, amount: number)
self.balance += amount
endExamples
Export a typed class instance shape
--!strict
export type Counter = typeof(setmetatable({} :: { value: number }, Counter))Keep method typing consistent with constructor output
--!strict
local counter = Counter.new(0)
Counter.increment(counter, 1)
counter:increment(1)Refinements
Key Concepts
- Refinement narrows a variable or property to a more specific type based on control flow.
- Luau refines through truthiness checks,
type(...)guards, equality tests, andassert(...). - Compound boolean expressions can refine multiple values at once.
- Tagged unions depend on refinements to safely access case-specific fields.
Rules
- Refine before reading fields or calling operations that only exist on one branch of a union.
- Use
type(x) == "..."for primitive narrowing. - Use equality against singleton values or discriminants for tagged unions.
- Use
assert(...)when a failure should stop execution and narrow afterward. - Keep branch logic simple enough that the narrowing remains obvious.
Patterns
Truthiness narrowing
--!strict
local maybeName: string? = nil
if maybeName then
local name: string = maybeName
endPrimitive type guard
--!strict
local function toNumber(value: string | number): number
if type(value) == "number" then
return value
end
return tonumber(value) or 0
endAssertion-based narrowing
--!strict
local value: string | number = "42"
assert(type(value) == "string")
local text: string = valueExamples
Narrow a discriminated union
--!strict
type Ready = { state: "ready", payload: string }
type Idle = { state: "idle" }
type Model = Ready | Idle
local function read(model: Model): string?
if model.state == "ready" then
return model.payload
end
return nil
endCompose guards
--!strict
local function normalize(x: string | number | nil): string?
if x and type(x) == "string" then
return x
end
return nil
endRoblox Types in Luau
Key Concepts
- Roblox classes, datatypes, and enums are available to the Luau type checker by name.
- Inheritance is modeled, so subtype values can flow into parent-typed positions.
- The analyzer understands common constructors such as
Instance.newand services returned bygame:GetService. IsAcan refine Roblox instance types in control flow.
Rules
- Use Roblox names only as type information in this skill.
- Prefer the most specific useful Roblox type at the API boundary.
- Rely on inheritance when a parameter only needs a parent capability such as
InstanceorBasePart. - Use
Enum.<Name>for enum typing. - Use
IsAchecks to narrow instance unions or parent types before accessing subtype members. - Do not expand into networking, persistence, or broader Roblox architecture.
Patterns
Class and datatype annotations
--!strict
local part: Part = Instance.new("Part")
local position: Vector3 = part.Position
local material: Enum.Material = part.MaterialAccept a parent type when that is enough
--!strict
local function rename(instance: Instance, name: string)
instance.Name = name
endRefine with IsA
--!strict
local function getTextLabelText(instance: Instance): string?
if instance:IsA("TextLabel") then
return instance.Text
end
return nil
endExamples
Let Luau infer constructor result types
--!strict
local folder = Instance.new("Folder")
local asInstance: Instance = folderNarrow a broad input to a specific GUI type
--!strict
local function readGuiText(instance: Instance): string
if instance:IsA("TextButton") or instance:IsA("TextBox") then
return instance.Text
end
return ""
endType Functions
Key Concepts
- Type functions run during analysis time, not runtime.
- They operate on types and can build or transform types programmatically.
- They are appropriate for advanced reusable patterns that cannot be expressed clearly with plain aliases alone.
- The
typeslibrary provides primitives for inspecting and constructing types.
Rules
- Use type functions only when simpler aliases, generics, unions, or intersections are insufficient.
- Keep type functions deterministic and narrowly scoped to type transformation.
- Validate inputs and raise clear errors when a type function expects a certain shape.
- Prefer a named alias wrapper around complex type-function results so consumers see a stable API.
- Do not drift into runtime logic; these execute during analysis only.
Patterns
Compute keys from a table type
type function simple_keyof(ty)
if not ty:is("table") then
error("expected table type")
end
local union = nil
for property in ty:properties() do
union = if union then types.unionof(union, property) else property
end
return if union then union else types.singleton(nil)
endWrap a type-function result in an alias
type Person = {
name: string,
age: number,
}
type PersonKeys = simple_keyof<Person>Examples
Restrict advanced logic to type-level utilities
type function require_table(ty)
if not ty:is("table") then
error("table expected")
end
return ty
end
type Checked = require_table<{ id: number }>Use plain aliases when that is enough
--!strict
type IdMap<T> = {[string]: T}Type System Overview
Key Concepts
- Luau is gradually typed: the analyzer combines inference and explicit annotations.
- File mode changes analyzer behavior:
--!strictkeeps inference precise and reports more issues early.--!nonstrictfalls back toanymore easily.--!nocheckdisables analysis for the file.- Luau is structurally typed, especially for table shapes.
- Annotations document intent; casts with
::adjust overly broad inferred types.
Rules
- Prefer
--!strictfor new code and stable modules. - Use
--!nonstrictonly when migrating code that cannot be made strict yet. - Use
--!nocheckonly when analysis must be disabled intentionally. - Annotate module boundaries, important locals, and signatures whose intent is not obvious from inference.
- Use casts only when one side is a subtype of the other or
any; do not use them to hide design problems. - Remember that casting a multi-return expression preserves only the first value.
Patterns
Use strict mode to catch drift early
--!strict
local total
total = 1
total = total + 2Annotate the contract, not every temporary
--!strict
type Person = {
name: string,
age: number,
}
local function greet(person: Person): string
return "Hello, " .. person.name
endCast only to recover precision
--!strict
local data = {
names = {} :: {string},
}Examples
Structural typing with optional fields
--!strict
type Basic = { x: number }
type Extended = { x: number, y: number? }
local value: Basic = { x = 1, y = 2 }
local widened: Extended = { x = 1 }Export a shared alias from a module
--!strict
export type Result = {
ok: boolean,
message: string?,
}Unions and Intersections
Key Concepts
- A union
A | Bmeans a value is one of several possible types. - An intersection
A & Bmeans a value satisfies all combined types. - Tagged unions use a shared discriminant field to support safe refinements.
- Intersections are useful for combining compatible record capabilities or function signatures.
Rules
- Use unions for real alternative states, not as a replacement for missing design decisions.
- Prefer tagged unions over loose optional-field mixtures when cases differ structurally.
- Refine a union before using case-specific fields.
- Use intersections only when the combined shape is coherent.
- Do not write impossible primitive intersections such as
string & number.
Patterns
Tagged union for operation results
--!strict
type Ok<T> = { kind: "ok", value: T }
type Err<E> = { kind: "err", error: E }
type Result<T, E> = Ok<T> | Err<E>Combine record capabilities with an intersection
--!strict
type Named = { name: string }
type Timed = { duration: number }
type NamedTimed = Named & TimedOverloaded function type declarations
--!strict
type Parse = ((string) -> number) & ((number) -> string)Examples
Refine a tagged union by discriminant
--!strict
local function unwrap(result: Result<number, string>): number?
if result.kind == "ok" then
return result.value
end
return nil
endRequire both parts of an intersection
--!strict
local value: NamedTimed = {
name = "clip",
duration = 3.5,
}