roblox-luau-types

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Luau Type System

Luau类型系统

When to Use

使用场景

Load this skill when the task involves:
  • Adding or correcting type annotations on variables, functions, tables, modules
  • Choosing between
    --!strict
    ,
    --!nonstrict
    , and
    --!nocheck
  • Designing APIs that preserve inference instead of collapsing to
    any
  • Modeling data with generics, unions, intersections, optionals, tagged unions
  • Typing object-like tables, metatable-backed modules, exported module surfaces
  • Type narrowing with
    typeof()
    ,
    IsA()
    , or conditional checks
  • Understanding when to annotate vs when to let inference work
  • Cross-module type exports and contracts
Hand off to other skills when:
  • General Luau syntax, tables, control flow, string patterns →
    roblox-luau-core
  • OOP implementation, async patterns, module architecture →
    roblox-luau-patterns
  • Roblox engine APIs, networking, data storage →
    roblox-*
    domain skills
当任务涉及以下内容时加载此技能:
  • 为变量、函数、表、模块添加或修正类型注解
  • --!strict
    --!nonstrict
    --!nocheck
    之间选择合适的模式
  • 设计可保留类型推断而非退化为
    any
    的API
  • 使用泛型、联合类型、交叉类型、可选类型、标记联合类型建模数据
  • 为类对象表、元表驱动模块、导出模块表面添加类型定义
  • 使用
    typeof()
    IsA()
    或条件检查进行类型收窄
  • 理解何时需要注解、何时可依赖类型推断
  • 跨模块的类型导出与契约定义
在以下场景移交至其他技能:
  • 通用Luau语法、表、控制流、字符串模式 →
    roblox-luau-core
  • OOP实现、异步模式、模块架构 →
    roblox-luau-patterns
  • Roblox引擎API、网络通信、数据存储 →
    roblox-*
    领域技能

Decision Rules

决策规则

  • Use
    --!strict
    for new or actively maintained code
  • Prefer inference-preserving designs over annotation-heavy designs when inferred shape stays precise
  • Annotate where it clarifies intent, stabilizes contracts, constrains
    self
    , or prevents widening to
    any
  • Prefer explicit exported aliases at module boundaries for stable contracts
  • Use generics when input/output relationships matter; never replace with
    any
  • Use tagged unions + refinements for multi-case structured values
  • Casts (
    ::
    ) are a precision tool, not a bypass — narrow overly generic inference, don't hide errors
  • 新代码或维护中的代码使用
    --!strict
  • 当推断的类型形状保持精确时,优先选择保留类型推断的设计,而非大量注解的设计
  • 在需要明确意图、稳定契约、约束
    self
    或防止类型拓宽为
    any
    时添加注解
  • 在模块边界优先使用显式导出别名以保证稳定契约
  • 当输入/输出类型存在关联时使用泛型;绝不使用
    any
    替代
  • 为多场景结构化值使用标记联合类型+细化操作
  • 类型转换(
    ::
    )是精准工具而非规避手段——用于收窄过度泛化的推断类型,而非隐藏错误

Philosophy

核心理念

The type system exists to catch bugs at analysis time without affecting runtime. The goal is not "annotate everything" but "let the type checker help you." Key principles:
  1. Inference first. If the type checker already knows the type, don't annotate it. Redundant annotations add noise and can become stale.
  2. Annotate boundaries. Function parameters, return types, and exported module surfaces benefit from explicit types. Internal locals usually don't.
  3. Preserve relationships. A generic
    <T>
    that carries a type through a transform is more valuable than
    any
    that erases it.
  4. Narrow, don't cast. Use
    typeof()
    ,
    IsA()
    , and conditional checks to narrow types. Use
    ::
    only when you genuinely know more than the checker.
  5. Sealed vs unsealed matters. An annotated table is sealed (no new fields). An unannotated local table accumulates fields until it leaves scope or gets returned.

类型系统的作用是在分析阶段捕获bug,同时不影响运行时。目标并非“给所有内容加注解”,而是“让类型检查工具辅助你”。核心原则:
  1. 优先推断。如果类型检查器已能识别类型,无需添加注解。冗余注解会增加噪音且可能过时。
  2. 注解边界。函数参数、返回类型和导出模块表面适合添加显式类型。内部局部变量通常不需要。
  3. 保留关联。能在转换过程中传递类型的泛型
    <T>
    比会擦除类型的
    any
    更有价值。
  4. 优先收窄,而非转换。使用
    typeof()
    IsA()
    和条件检查收窄类型。仅当你确实比检查器更了解类型时才使用
    ::
  5. 密封与非密封的区别很重要。已注解的表是密封的(无法添加新字段)。未注解的局部表会累积字段,直到离开作用域或被返回。

Strictness Modes

严格性模式

luau
--!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.
2025-2026 Update: The New Type Solver (GA Nov 2025) is faster and more accurate.
--!nonstrict
is now the default for all scripts. Prefer
--!strict
for anything you actively maintain.

luau
--!strict    -- 完整类型检查。对未解析类型报错。适用于新代码。
--!nonstrict -- 默认模式。对未解析类型发出警告但允许运行。适用于过渡代码。
--!nocheck   -- 完全禁用类型检查。仅适用于生成代码或遗留代码。
2025-2026更新: 新型别求解器(2025年11月正式发布)更快更准确。
--!nonstrict
现已成为所有脚本的默认模式。对于所有你正在维护的代码,优先使用
--!strict

Basic Type Annotations

基础类型注解

luau
-- 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
luau
-- 变量注解
local name: string = "Alice"
local health: number = 100
local isAlive: boolean = true
local data: any = nil -- 退出类型检查

-- 函数参数与返回类型
local function add(a: number, b: number): number
    return a + b
end

-- 可选参数
local function greet(name: string, title: string?): string
    if title then
        return `{title} {name}`
    end
    return name
end

Table Types

表类型

luau
-- 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,
    },
}
luau
-- 数组类型
local scores: { number } = { 100, 95, 87 }

-- 字典类型(索引器)
local config: { [string]: boolean } = {
    shadows = true,
    particles = false,
}

-- 记录类型(具体字段)
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,
    },
}

Sealed vs Unsealed Tables

密封表与非密封表

luau
-- 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
luau
-- 非密封:未注解的局部表会累积字段
local config = {}
config.debug = true    -- 合法,表为非密封状态
config.version = "1.0" -- 合法,仍可累积字段

-- 密封:一旦注解或被返回,无法添加新字段
local settings: { debug: boolean } = { debug = true }
settings.version = "1.0" -- 错误:类型中不存在'version'

-- 实际应用:在添加注解前完成表的构建
local data = {
    name = "Alice",
    level = 10,
}
-- 此时data是非密封的,你仍可添加字段
data.guild = "Warriors"

-- 但一旦将其传递给带类型的函数或返回,它就会变成密封状态

Union and Intersection Types

联合类型与交叉类型

luau
-- 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
luau
-- 联合类型:值可以是多种类型之一
local id: string | number = "abc123"
id = 42 -- 同样合法

-- 可选类型是T | nil的简写
local nickname: string? = nil -- 等价于string | nil

-- 适用于可能执行失败的函数返回值
local function findPlayer(name: string): Player?
    -- ...
    return nil
end

-- 用于状态机的标记联合类型(可区分联合类型)
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 -- 类型收窄为Ready<number>
    end
    return nil
end

Type Narrowing and Guards

类型收窄与守卫

luau
-- 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
luau
-- typeof用于收窄类型(支持Roblox类型,优先于type())
local function process(value: string | number)
    if typeof(value) == "string" then
        -- 此处value的类型已收窄为string
        print(string.upper(value))
    else
        -- 此处value的类型已收窄为number
        print(value * 2)
    end
end

-- 使用:IsA()检查Instance类型
local function handlePart(instance: Instance)
    if instance:IsA("BasePart") then
        -- instance的类型已收窄为BasePart
        instance.Anchored = true
        instance.BrickColor = BrickColor.new("Bright red")
    end
end

-- 使用assert收窄非空类型
local function getPlayerData(player: Player): PlayerData
    local leaderstats = player:FindFirstChild("leaderstats")
    assert(leaderstats, "Player missing leaderstats")
    -- leaderstats现在被收窄为非空类型
    return parseStats(leaderstats)
end

-- 模式:类型守卫函数
local function isWeapon(item: Item): boolean
    return item.category == "weapon"
end

Generics

泛型

luau
-- 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).
luau
-- 泛型函数:在转换过程中保留元素类型
local function first<T>(list: { T }): T?
    return list[1]
end

local name = first({ "Alice", "Bob" }) -- 推断为string?
local num = first({ 1, 2, 3 })         -- 推断为number?

-- 泛型类型别名
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

-- 泛型类风格模式
type Stack<T> = {
    items: { T },
    push: (self: Stack<T>, value: T) -> (),
    pop: (self: Stack<T>) -> T?,
    peek: (self: Stack<T>) -> T?,
}

-- 注意:在类型定义中,self是显式的(它是函数签名的一部分)。
-- 在实际方法定义中,使用:来隐藏self(详见roblox-luau-patterns)。

When to Use Generics

泛型的适用场景

  • Yes: When a function transforms input and the output type depends on the input type
  • Yes: When a container holds items of a specific type that callers should know about
  • Yes: When you want to preserve type relationships across a chain of operations
  • No: When the type is always the same (just use the concrete type)
  • No: When you'd end up with
    <any>
    everywhere (you've lost the benefit)
  • 适用:当函数转换输入且输出类型依赖于输入类型时
  • 适用:当容器存储特定类型的元素且调用者需要知晓该类型时
  • 适用:当你需要在一系列操作中保留类型关联时
  • 不适用:当类型始终相同时(直接使用具体类型即可)
  • 不适用:当最终到处都是
    <any>
    时(你已失去泛型的价值)

Type Exports

类型导出

luau
-- 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
luau
-- 在ModuleScript中,导出类型供其他模块使用
-- 文件:ReplicatedStorage/Types.lua

export type WeaponData = {
    name: string,
    damage: number,
    rarity: "Common" | "Rare" | "Epic" | "Legendary",
    durability: number,
}

export type InventorySlot = {
    item: WeaponData?,
    quantity: number,
}

-- 使用者通过require导入
-- 文件: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

Export Philosophy

导出理念

  • Export named aliases for every type that crosses a module boundary
  • Keep implementation types internal (don't export helper types only used inside)
  • Choose signatures that let callers infer types cleanly without needing to import the alias
  • A well-typed module surface acts as documentation
  • 为所有跨模块边界的类型导出命名别名
  • 保留内部实现类型(不要仅导出模块内部使用的辅助类型)
  • 选择能让调用者无需导入别名即可清晰推断类型的签名
  • 类型完善的模块表面可作为文档使用

Common Roblox Types

常见Roblox类型

luau
-- 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
luau
-- Instance层级类型
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

-- 值类型(这些不是Instance - 它们是值类型/结构体)
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)

-- 枚举类型
local material: Enum.Material = Enum.Material.Grass
local partType: Enum.PartType = Enum.PartType.Ball

Typing Object-Like Modules

为类对象模块添加类型

luau
--!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 Counter
luau
--!strict

-- 从setmetatable派生实例类型以实现精确的self类型
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

-- 当:语法无法精确推断self时,添加显式self注解
function Counter.increment(self: Counter, amount: number): number
    self.value += amount
    return self.value
end

return Counter

When to Use Explicit
self

何时使用显式
self

  • When the type checker can't infer
    self
    precisely through
    :
    syntax
  • When you need
    self
    to be a specific subtype in an inheritance chain
  • When the method is defined with
    .
    but called with
    :
    (rare, avoid if possible)
  • In type definitions (function signatures in type aliases always need explicit self)

  • 当类型检查器无法通过
    :
    语法精确推断
    self
  • 当你需要
    self
    成为继承链中的特定子类型时
  • 当方法用
    .
    定义但用
    :
    调用时(罕见,尽可能避免)
  • 在类型定义中(类型别名中的函数签名始终需要显式self)

Common Mistakes

常见错误

  • Leaving variables unannotated in
    --!nonstrict
    → unintentional
    any
    propagation
  • Replacing useful generic relationships with
    any
    or overly broad unions
  • Sealing a table too early with an annotation, then expecting to add fields later
  • Expecting
    :
    method definitions to automatically share precise
    self
    type across the class
  • Using
    ::
    to force unrelated conversions instead of fixing underlying type design
  • Building unions without a discriminant, making downstream refinement difficult
  • Using intersections between incompatible primitives (
    string & number
    )
  • Annotating every local variable (noise that hides the important annotations)
  • Exporting internal helper types that clutter the module's public surface
  • --!nonstrict
    中保留未注解的变量 → 意外的
    any
    类型传播
  • any
    或过于宽泛的联合类型替代有用的泛型关联
  • 过早用注解密封表,之后又想添加字段
  • 期望
    :
    方法定义能自动在类中共享精确的
    self
    类型
  • 使用
    ::
    强制无关类型转换,而非修复底层类型设计
  • 创建无判别字段的联合类型,导致下游细化操作困难
  • 在不兼容的原始类型间使用交叉类型(如
    string & number
  • 为每个局部变量添加注解(噪音会掩盖重要注解)
  • 导出内部辅助类型,导致模块公共表面杂乱

Quality Checklist

质量检查清单

  • File has appropriate strictness mode (
    --!strict
    for maintained code)
  • Function parameters and return types annotated at module boundaries
  • Internal locals rely on inference where the inferred type is precise
  • Generics preserve type relationships (no
    any
    escape hatches)
  • Tagged unions have a discriminant field for narrowing
  • Exported types are named, focused, and documented
  • Casts (
    ::
    ) are justified (narrowing, not hiding errors)
  • No sealed table violations (fields added after annotation)
  • 文件使用了合适的严格性模式(维护中的代码使用
    --!strict
  • 模块边界处的函数参数和返回类型已注解
  • 内部局部变量在推断类型精确时依赖类型推断
  • 泛型保留了类型关联(无
    any
    规避手段)
  • 标记联合类型有用于收窄的判别字段
  • 导出的类型命名清晰、聚焦且有文档说明
  • 类型转换(
    ::
    )有合理理由(用于收窄,而非隐藏错误)
  • 无密封表违规(注解后添加字段)