typescript-best-practices について
FiveMリソース開発者向けのTypeScriptガイダンススキルです。有効な状態を型で表し、型付けされていない実行時境界でデータを検証して、業務ロジックの型安全性を高めます。
主な機能
- 01判別共用体でバリアントをモデル化
- 02ブランド型で識別子を区別
- 03境界で型なしデータを検証
- 04unknownと検証済み型ガードを優先
- 05純粋ロジックをテストし有用な情報を記録
活用シーン
- 01FiveMネットワークイベント入力の型付け
- 02NUIと永続化データの検証
- 03相互排他的な状態のモデル化
- 04リソースコードの危険な型アサーション削減
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ overextended/ skills --skill typescript-best-practicesインストール後、Claude Code で/typescript-best-practicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
CREDITS.mdPATTERNS.mdagents/openai.yaml詳細
TypeScript のベストプラクティス
コンパイラーに、不可能な状態、取り違えた ID、未処理のバリアントを拒否させましょう。FiveM リソースでは、型安全性の大半がランタイムとの境界で失われます。onNet、exports、state bag、NUI コールバックは any または Function として型付けされているため、そこに型注釈を付けても、何のチェックも行われないキャストにすぎません。境界で解析し、それ以外では型を信頼しましょう。
原則
- 不正な状態を表現できないようにします。 あるフィールドの組み合わせが有効になる条件を説明するコメントを書けるなら、判別可能なユニオンに分割しましょう。
- 型を制限するのではなく、型を組み立てます。 緩い型にランタイムチェックを加えるのではなく、不正な値を保持できない形(先頭と残り、開始時刻と期間など)を選びましょう。
- コンパイラーに嘘をつきません。 事実を証明できないなら、検証、絞り込み、モデルの再設計によって証明しましょう。すべての
any、as、!を境界までたどり、その場で検証してください。 - 不完全性が現れる箇所だけ、型を強化します。
!、キャスト、「決して起こらないはず」というスローは、型が弱すぎることを示しています。そのチェックを型に反映し、それ以上は手を加えないようにしましょう。 - 境界で検証し、内部では信頼します。 ビジネスロジックは、型付きデータを受け取る単純な関数に置きます。その周囲のイベントハンドラーは薄くし、解析、呼び出し、応答だけを行います。
ルール
| ルール | 概要 |
|---|---|
| 判別可能なユニオン | kind のリテラル判別子でバリアントをモデル化し、表現できない状態を作ります。省略可能なフィールドを詰め込んだオブジェクトは使いません。 |
| ブランド型 | & { readonly __brand: "X" } でプリミティブ型にブランドを付け、サーバー ID、キャラクター ID、アイテム名を取り違えないようにします。境界で一度だけ検証します。 |
| 構成的モデリング | 不正な値を構築できない形にします。空でない値には [T, ...T[]]、偶数個の値には [T, T][]、範囲には start と duration を使います。 |
| 最も単純な全域型 | すべての操作が全域的である間は T[] のままにします。緩い型によって ! や型アサーション、「起こるはずのない」例外が必要になる場合に限り、NonEmpty<T> に強化します。 |
any より unknown | リソースの外部から来るデータは unknown にします。ネットイベントハンドラーの引数も含みます。 |
| パース関数 | unknown を受け取り、値または null を返す関数で、クライアントデータを型付きの値に変換します(toCoords(value: unknown): Vec3 | null)。新しいものを書く前に、リソースに既存のパーサーや型ガードを再利用します。スキーマライブラリは追加しません。FiveM リソースでは使用されていません。 |
null は省略を意味する | FiveM はイベント引数を MessagePack でシリアライズするため、省略された値や undefined が null になることがあります。省略可能な値は != null でチェックし、!== undefined だけで判定しないでください。また、値は真偽値ではなく、型と範囲で必須かどうかを判定します。0 と false は有効な値です。 |
as による型アサーションは禁止 | すべての as は、いつ実行時クラッシュを起こしてもおかしくありません。検証後にのみ型アサーションを使います。triggerServerCallback<T> のような型引数も型アサーションです。 |
| 型の絞り込みの優先順位 | 判別子による switch > in 演算子 > typeof/instanceof > ユーザー定義の型ガード > as。 |
| 型ガード | 主張の内容を検証する必要があります。嘘をつく型ガードは、問題が安全だと示す名前の陰に隠れるため、as よりも悪質です。isX または hasX という名前にします。 |
| 網羅性 | default 節に const _exhaustive: never = x; をインラインで記述し、新しいバリアントが追加されたときにコンパイラーがエラーを出すようにします。 |
as より satisfies | リテラル型を拡大せずに値を検証できます。 |
| 境界での検証 | データが入ってくる境界で、名前付きドメイン型にパースします。内部では型を信頼します。以下の FiveM の境界を参照してください。 |
| 派生型 | 新しいインターフェイスを宣言する前に、Pick/Omit/Parameters/ReturnType/Awaited/typeof を検討します。クライアントスクリプトと NUI では、型を二重に宣言せず、同じ型を共有します。 |
| オブジェクト引数 | 位置引数ではなくオブジェクトを渡し、引数の順序がひと目でわかるようにします。ホットパス(setTick コールバックや、毎フレーム実行される処理)では使わず、オブジェクト引数にするためだけにネイティブをラップしないでください。 |
| 実際に動かすテスト | 実行できるものはモックしません。リソースにテストランナーがある場合、通常のロジックには実際のユニットテストを用意します。ネイティブ、イベント、NUI は、稼働中のサーバーで検証します。 |
| 役立つログ | ID からデバッグできるだけのコンテキスト(プレイヤー ID、イベント名、リソース)を含めてログに記録します。リリースコードにデバッグ用の console.log を残しません。 |
FiveM の境界
以下はすべて、コンパイラーが認識しないデータです。受け取った場所で解析してください。
| 境界 | 型付けされていない理由 |
|---|---|
サーバー上の onNet ハンドラーの引数 | どのクライアントでも、任意の引数でイベントを発生させられます。onNet は Function を受け取るため、パラメーターの型注釈は検証されません。 |
onClientCallback の引数(@overextended/ox_lib/server) | ...args: any[] として型付けされており、クライアントから渡されます。 |
RegisterNuiCallback のデータ | ブラウザーから渡されます。プレイヤーは開発者ツールでブラウザーを操作できます。サーバーは、クライアントが転送するデータをすべて再確認します。 |
exports.<resource>.<name>() の戻り値 | すべてのエクスポートは Function として型付けされているため、戻り値は any です。型付き関数で一度ラップしてください。 |
State bag(GlobalState、Player(id).state、Entity(id).state) | any としてインデックス化されています。クライアントが書き込めるキーは、クライアントデータとして扱ってください。 |
triggerServerCallback<T> の応答(@overextended/ox_lib/client) | T は検証されず、戻り値の型は Promise<T> | void です。delay のレート制限により呼び出しが抑止されると、何も返されずに解決します。 |
JSON.parse、KVP 文字列、convar、データベースの行、アイテムのメタデータ | 文字列または型付けされていない行です。永続化する JSON にはバージョンフィールドを設け、解析を try / catch で囲んでください。 |
サーバーでは、すべてのハンドラーの先頭で、await を実行する前に source をローカル変数へコピーしてください。これは次のイベントで上書きされるグローバル変数です。
リソースで TypeScript から ox_lib を使う場合は、exports.ox_lib を直接呼び出す代わりに @overextended/ox_lib をインポートしてください。このパッケージは、型付きのエクスポートを提供します。
例
各ルールのコード例については、PATTERNS.md を参照してください。
コメント
Markdown 対応