mcpField
L
luau-expert

luau-expert

github.com/afrxo開発1270 回インストール26 週間前に更新
リポジトリ

luau-expert について

Luau開発者向けに、現代的な厳密型付けの書き方をまとめたスキルです。型、構文、モジュール、検証の指針と例を示し、既存プロジェクトの規約を尊重して適用する方針も説明します。

主な機能

  1. 01厳密な型付けと型システムの指針
  2. 02現代的なLuau構文と慣用表現
  3. 03モジュールと不変性のパターン
  4. 04信頼境界での入力検証
  5. 05アンチパターンと検証ルール
  6. 06リファレンスとコード例

活用シーン

  1. 01新しい厳密型付けLuauファイルの作成
  2. 02明示的に依頼されたコードのリファクタリング
  3. 03Luauの型とAPI境界の確認
  4. 04プロジェクトの規約に沿った言語パターンの適用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/afrxo/roblox-agent-skills --skill luau-expert

インストール後、Claude Code で/luau-expertを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

examples/
.gitkeep
bad.luau
exhaustive-match.luau
good.luau
module-pattern.luau
remote-validation.luau
references/
type-functions-api.md
type-patterns.md
type-system.md
typed-class-recipe.md

詳細

Luau 言語

モダンで厳密に型付けされた Luau を記述します。ソース:luau.org。

詳しくは以下をご覧ください。

さらに詳しくは、references/type-functions-api.md(解析時の types ライブラリ)、references/type-patterns.md(ブランド型/再帰型、オーバーロード、読み取り専用テーブル)をご覧ください。


統合ポリシー

このスキルでは推奨パターンを説明しますが、既存コードを書き換えることを許可するものではありません。新しいコードと、ユーザーから明示的にリファクタリングを依頼されたコードにルールを適用してください。スキルの既定値を適用する前に、コードベースの既存スタイル(大文字・小文字の使い分け、インデント、引用符のスタイル、モジュールの構成)に合わせてください。より厳格なファイルの制約を緩めないでください。プロジェクトで現在使われていないツールを導入しないでください。競合があれば一度だけ提示し、既存のパターンを優先してください。

すべてのルールについては、../../shared/integration-policy.mdをご覧ください。


原則

  • 厳密に型付けします。 新しいファイルでは --!strict を使います。レガシーコードは移行します。strict なファイルの制約を緩めてはいけません。
  • 解析時にバグを検出できるなら、実行時のアサーションより優先します。 型付きパラメーターは assert(typeof(x) == "number") より価値があります。
  • 巧妙さよりシンプルさを優先します。 小さな関数で、意図が明確になるようにします。
  • 不変性を優先します。 yield 中に共有状態を変更すると、あらゆるものが壊れます。React スタイルのライブラリでは、これは譲れません。
  • 公開 API はすべて型付けし、単純なローカル変数は省略します。 他のモジュールから呼び出せるものには注釈を付けます。local n = item.attack(p) のような記述には注釈を付けません。型推論で処理できます。

モードヘッダー

--!strict      -- default for new files; flags anything that *might* fail at runtime
--!nonstrict   -- legacy migration; infers any when type unknown
--!nocheck     -- generated/third-party only

プロジェクト全体の設定:{ "languageMode": "strict" } を指定した .luaurc。


モダンな構文

-- String interpolation (not string.format / not concat)
print(`Player {name} scored {score} points`)

-- if-then-else expression (not `a and b or c`)
local label = if score >= 100 then "Winner" else "Try again"

-- Generalized iteration (not pairs/ipairs)
for k, v in t do ... end

-- Compound assignments
count += 1
path ..= "/suffix"

-- Continue
for _, item in items do
    if not item.active then continue end
    process(item)
end

-- Floor division
local q = 7 // 2

-- Const bindings (binding immutable; value still mutable)
const MAX_PLAYERS = 16

-- Number separators
local big = 1_000_000

-- Function attributes
@native
local function hotPath(x: number): number
    return x * x + x
end

疎な配列で最初の nil で停止する動作が特に必要な場合に限り、ipairs を使います。


イディオム

早期リターン/continue

条件節をネストするより、ガード節を使います。関数が論理的に続行できない場合は、早期にリターンします。

local function dealDamage(humanoid: Humanoid, damage: number)
    if damage <= 0 then return end
    humanoid.Health -= damage
end
明示的な nil チェック

多くの場合、「truthy」チェックが実際に意味するのは「nil ではない」です。意図をそのまま記述してください。

if x.Parent ~= nil then ...   -- ✅
if x.Parent then ...          -- ❌ ambiguous

truthy を使ってよい例外は 2 つあります。if 式(反転した形式より if x then a else b のほうが読みやすい場合)と、or によるデフォルト値の指定です。

デフォルト値には or を使い、x and y or z は使わない
soundClone.Volume = volume or 0.5

-- Never simulate ternary with and/or — silently wrong when middle value is falsy
local goldAmount = if gamemode == "arena" then nil else 100   -- ✅
-- local goldAmount = gamemode == "arena" and nil or 100      -- ❌ returns 100 even on arena
組み込み関数を隠さない
local insert = table.insert    -- ❌
table.insert(items, sword)     -- ✅
呼び出し糖衣構文を使わない
call("string")    -- ✅
call({ ... })     -- ✅
call "string"     -- ❌
call { ... }      -- ❌ (StyLua reformats)
yield する関数には Async サフィックスを付ける
local function fetchPlayerDataAsync(userId: number): PlayerData
    -- yields here
end

React(および同様のレンダリングライブラリ)は yield を想定していません。yield する呼び出しに遭遇した再レンダーは、コンポーネントの状態を気付かれないまま破損させます。yield する関数に名前で印を付けると、呼び出し側からそれが分かります。

pcall のメソッド短縮記法
pcall(part.Destroy, part)                       -- ❌ reads as nonsense
pcall(function() part:Destroy() end)            -- ✅
pcall(saveMoney, player, 100)                   -- ✅ function-shorthand reads naturally

require

  • 動的な require は禁止です。 require(Modules.X) では型が維持されますが、ループ内の require(child) では型情報が失われます。
  • アルファベット順のブロックを 1 つだけ使います。 パッケージとローカルでセクションを分けないでください。Luau LSP の auto-require と StyLua の sort_requires を使います(roblox-tooling で説明)。

型システム(要点)

-- Optional
type Maybe<T> = T?              -- T | nil

-- Tagged union (preferred for results)
type Result<T> = { ok: true, value: T } | { ok: false, err: string }

-- Indexed maps where misses are expected
local playerPoints: { [Player]: number? } = {}    -- ✅ checker forces nil-handling
-- { [Player]: number }                            -- ❌ hides bugs on missing keys

-- Optional argument typing — ? at outer position
local function f(default: (boolean | () -> boolean)?) end    -- ✅
-- local function f(default: boolean? | () -> boolean) end   -- ❌

信頼できない入力には unknown を使い、使用前に型を絞り込みます。any は使わず、常にまず unknown を使ってください。

全体については、references/type-system.mdをご覧ください。

nil は何もないこととは異なります
local function returnsNothing() end          -- zero values
local function returnsNil() return nil end   -- one value: nil

print(returnsNothing())   -- (blank)
print(returnsNil())       -- nil

分岐間で戻り値の形を一貫させます。関数が T? を返す場合、値がない分岐では、値なしの return ではなく return nil を使います。

文字列列挙型+exhaustiveMatch

使う列挙型の形式はこれだけです。

type Color = "red" | "green" | "blue"

local function exhaustiveMatch(value: never): never
    error(`Unknown value in exhaustive match: {value}`)
end

local function setColor(color: Color)
    if color == "red" then ...
    elseif color == "green" then ...
    elseif color == "blue" then ...
    else exhaustiveMatch(color)   -- type error if a branch is missed
    end
end

makeEnum({...}) ライブラリや、Color = { red = "red" :: Color } のようなラッパーは使わないでください。文字列ユニオン型でできること以上のものは得られません。

examples/exhaustive-match.luauをご覧ください。


アサーション

  • エラーメッセージは必ず渡してください。 assert(cond) のようにメッセージを省略すると、役に立たない assertion failed! が表示されます。
  • メッセージには定数のみを使ってください。 assert は呼び出すたびにメッセージを評価します。書式設定が必要な場合は、if not cond then error(\...`) end` を使ってください。
  • 型を補うために assert(typeof(x) == ...) を使わないでください。 コードが厳密に型付けされている場合、この行は型付けされていない呼び出し元を経由しない限り実行されません。上流の型を修正してください。
  • 信頼境界(RemoteEvents、JSON、プラグイン入力)では、入力を unknown として型付けし、型を絞り込んでください。
HurtMe.OnServerEvent:Connect(function(player, damage: unknown)
    if typeof(damage) ~= "number" or damage < 0 then return end
    -- damage: number from here
    dealDamage(player, damage)
end)

パラメーターを number として型付けすると、悪意のあるユーザーが文字列を送信した場合でも、実行時チェックが静的に有効であるかのように見えてしまいます。unknown を使えば、チェックが必須になります。

examples/remote-validation.luau を参照してください。


モジュール

--!strict
local OtherModule = require("./OtherModule")

export type Config = { timeout: number, retries: number }

const DEFAULT_CONFIG: Config = { timeout = 30, retries = 3 }

local MyModule = {}

function MyModule.connect(cfg: Config?): ()
    local c = cfg or DEFAULT_CONFIG
    -- ...
end

return table.freeze(MyModule)
  • 名前付きのローカル変数に必ず代入し、それを return してください。 return function() ... end のような書き方は避けてください。Web や rg で Ctrl-Shift-F を使った検索が機能しなくなります。
  • ユーティリティごとにファイルを1つ作ってください。 TableUtil のような巨大モジュールは避け、flatten.luau と reverse.luau を別々に作ってください。Luau LSP は名前だけで自動的に require します。プロジェクト構成の根拠は roblox-systems にあります。
  • 返すモジュールテーブルには table.freeze を適用してください。 誤った変更を検出できます。
  • 循環依存: 循環を解消するには、インポートしたモジュールを any にキャストし、その理由を記載してください。
ディープコピーではなくシャローコピー

イミュータブルな更新では、変更するパスだけをクローンしてください。

items = table.clone(items)
items[1] = table.clone(items[1])
items[1].durability -= 10

イミュータビリティのもとでは、ディープコピーは常に無駄な処理です。無関係なサブツリーは同じアイデンティティを保ちます。これは、レンダー間で == がチェックする内容そのものです。

examples/module-pattern.luau を参照してください。


OOP — デフォルトはフリー関数

クラスよりも、プレーンなデータとフリー関数を使ってください。メタテーブルは型チェッカーと相性が悪く、grep しづらく、読者を困惑させます。

type Slide = { length: number }
type Video = { slides: { Slide } }

-- videoLength.luau
local function videoLength(video: Video): number
    local total = 0
    for _, slide in video.slides do
        total += slide.length
    end
    return total
end

return videoLength

__index と __tostring(および、まれに使う弱テーブル用の __mode)を除き、あらゆるメタメソッドを避けてください。 __call や __add、演算子オーバーロードは使わないでください。これらはオートコンプリートされず、型推論の妨げになります。

クラスが本当に必要な場合は、references/typed-class-recipe.md を参照してください。


検証ルール

  • Lua の API が Luau にも存在すると決めつけないでください。 削除または移動された API がいくつかあります。
    • setfenv / getfenv — 削除されました。
    • unpack(グローバル)— table.unpack に移動しました。
    • loadstring — 削除されました。明示的に有効化されている場合に限り、loadstring 相当の機能を loadstring 経由で使えます。それ以外の場合は利用できません。
    • bit32 — 利用できますが、可能な場合はネイティブのビット演算子を検討してください。
    • goto / ラベル — Luau にはありません。
  • 構文を捏造しないでください。 const、@native、型関数、デフォルトジェネリクスなどの機能が存在するか、安定しているか不明な場合は、使用する前に luau.org を確認してください。推測するのではなく、返信で不確実であることを伝えてください。
  • 壊れやすいシグネチャより、安定した概念を優先してください。 「この正確な標準ライブラリ関数がこの正確な引数を受け取る」ではなく、「型システムを使ってバグを防ぐ」と表現できる場合は、そうしてください。Luau の標準ライブラリは進化しますが、型付けの原則は変わりません。
  • Roblox API はここでは対象外です。 RemoteEvent、DataStore、Instance、サービスなどについては、roblox-dev スキルを参照してください。このスキルの対象は言語のみです。

アンチパターン

❌ 避ける✅ 推奨
pairs(t) / ipairs(t)for k, v in t do
string.format(...)`interpolated {value}`
a and b or cif a then b else c
if x.Parent thenif x.Parent ~= nil then
local insert = table.inserttable.insert を直接使う
call "x" / call { ... }call("x") / call({ ... })
再代入可能な定数const MAX = 100
グローバル変数local / const
anyunknown + 絞り込み
自明なローカル型注釈型推論に任せる
型指定のない公開関数引数と戻り値に型注釈を付ける
型を修正するための assert(typeof(x) == "T")上流の型を修正する
メッセージなしの assert(cond)/書式付きメッセージ定数メッセージを使う。書式付きの場合は if … then error(…) を使う
pcall(part.Destroy, part)pcall(function() part:Destroy() end)
キーが欠けている可能性がある場合の { [K]: V }{ [K]: V? }
T? 関数からの単独の return明示的に return nil とする
for _, m in c:GetChildren() do require(m)静的な require(c.X)
セクション分けされた require ブロックアルファベット順の単一ブロック
Async 接尾辞のない yield 関数Async を接尾辞に付ける
makeEnum({"red","green"})type Color = "red" | "green" + exhaustiveMatch
__call / __add / 演算子メタメソッド通常の関数
匿名のモジュール戻り値local foo = ...; return foo
巨大な TableUtil モジュールファイルごとに関数を1つにする
不変更新のためのディープコピー変更するパスだけを複製する
math.floor(a / b)a // b
モジュールテーブルの変更戻り値を table.freeze する
ピラミッド状のネスト早期リターン + continue

例

コメント