mcpField
T
typescript-best-practices

typescript-best-practices

github.com/overextended开发208 次安装2 周前更新
代码仓库

关于 typescript-best-practices

面向 FiveM 资源开发者的 TypeScript 指导技能,强调建模有效状态,并在运行时边界验证无类型数据,让业务逻辑依赖更可靠的类型。

主要功能

  1. 01用可辨识联合建模变体
  2. 02用品牌类型区分标识符
  3. 03在边界验证无类型数据
  4. 04优先使用 unknown 和可信类型守卫
  5. 05测试纯逻辑并记录有效上下文

适用场景

  1. 01为 FiveM 网络事件输入添加类型
  2. 02验证 NUI 和持久化数据
  3. 03建模互斥状态
  4. 04减少资源代码中的不安全类型断言

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/overextended/skills --skill typescript-best-practices

安装后在 Claude Code 中输入/typescript-best-practices确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

CREDITS.md
PATTERNS.md
agents/
openai.yaml

详细介绍

TypeScript 最佳实践

让编译器拒绝不可能的状态、混淆的 ID 和未处理的变体。在 FiveM 资源中,大部分类型安全性会在运行时的边界处流失:onNet、exports、状态包和 NUI 回调的类型都是 any 或 Function,因此那里的类型注解只是一个未经检查的强制转换。在这些边界处解析数据,然后在其他地方信任类型。

原则

  • 让非法状态无法表示。 如果你需要写注释来说明某种字段组合何时有效,就把它拆分为判别联合类型。
  • 构建类型,而不是限制类型。 选择无法容纳非法值的结构(头部加剩余部分、起始值加持续时间),而不是宽松类型加运行时检查。
  • 不要欺骗编译器。 如果它无法证明某个事实,就去证明它(验证、收窄类型、重新建模)。追踪每个 any、as 和 !,直到边界处,并在那里进行验证。
  • 仅在出现不完整性时强化类型。 !、强制转换或“绝不应该发生”的抛错,都表明类型过于宽松。将检查推到类型中,然后就此打住。
  • 在边界处验证,在内部信任。 业务逻辑放在接收类型化数据的普通函数中。外围事件处理器应保持精简:解析、调用、回复。

规则

规则摘要
判别联合类型使用 kind 字面量判别字段对变体建模,使不可能的状态无法表示。不使用可选字段集合。
品牌类型使用 & { readonly __brand: "X" } 为原始类型添加品牌,避免混淆服务端 ID、角色 ID 和物品名称。在边界处验证一次。
构造式建模设计类型结构,使非法值无法构造。非空数组使用 [T, ...T[]],偶数长度数组使用 [T, T][],范围使用 start 加 duration。
最简单的全定义类型只要对 T[] 的所有操作都保持全定义,就继续使用 T[]。只有当宽松类型迫使你使用 !、类型断言或“理论上不可能发生”的异常时,才加强为 NonEmpty<T>。
优先使用 unknown 而非 any来自资源外部的数据都是 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;,这样添加新变体时编译器会报错。
优先使用 satisfies 而非 as验证值,同时不拓宽字面量类型。
边界验证在数据进入系统的边界处,将其解析为命名的领域类型。内部代码信任类型。参见下文的 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。用类型化函数包装一次即可。
状态包(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。

在服务器上,每个处理程序的开头都要先将 source 复制到局部变量,再执行任何 await:它是一个全局变量,下一个事件会覆盖它。

如果资源使用 TypeScript 中的 ox_lib,请导入 @overextended/ox_lib,而不要直接调用 exports.ox_lib:该包会为导出内容添加类型。

示例

阅读 PATTERNS.md,查看每条规则对应的代码示例。

评论

支持 Markdown