TanStack Query ESLint 规则 mutation-property-order:useMutation 中类型推断敏感属性的顺序规范与实现剖析
本篇聚焦 TanStack Query 的 ESLint 插件规则 mutation-property-order(文档:docs/eslint/mutation-property-order.md)。它强制 useMutation() 配置对象中 onMutate、onError、onSettled 三个属性按特定顺序书写,以保障 TypeScript 对回调参数的类型推断正确。读完后你将理解:为什么对象属性书写顺序会影响类型推断、该规则的检查范围与豁免范围、它在插件源码中的检测与自动修复链路,以及如何在项目中启用这条可自动修复(Fixable)的推荐规则。
规则要解决的问题:属性顺序为何影响类型推断
在 useMutation() 中,onError 和 onSettled 回调会收到 onMutate 的返回值(通常作为恢复快照的 context / onMutateResult 参数)作为入参。文档明确指出:由于类型推断(type inference)的原因,传入对象的属性顺序是有意义的,其中仅以下属性对顺序敏感:
useMutation()
正确的属性顺序为:
onMutateonErroronSettled
其余所有属性(如 mutationFn、onSuccess、retry、gcTime 等)不参与顺序约束,因为它们不依赖类型推断链路。
问题出在 onMutate 的返回类型是 TContext 的推断来源。TypeScript 对对象字面量做上下文类型推断时,属性按书写顺序参与泛型推断;当 onError / onSettled 写在 onMutate 之前时,此时 onMutate 尚未被“看见”,其返回类型还未纳入推断,这两个回调的 context 参数就难以被推断为 onMutate 返回的具体类型(例如 { backup: string }),从而退化为宽泛的 TContext | undefined 类型,逼迫开发者手写类型标注,甚至掩盖类型错误。文档给出的反例正是这种场景:onSettled 与 onError 抢在 onMutate 前面,规则即会报告 Invalid order of properties for useMutation. 并给出自动重排修复。
规则元信息
从规则源码 mutation-property-order.rule.ts 可以确认其元信息,与文档中的 Attributes 小节(✅ Recommended、🔧 Fixable)一致:
| 元信息 | 取值 | 说明 |
|---|---|---|
| 规则名 | mutation-property-order |
配置时写作 @tanstack/query/mutation-property-order |
| type | problem |
归类为正确性问题,而非风格问题 |
| recommended | error |
在插件的推荐配置中以 error 级别开启 |
| fixable | code |
提供自动修复,直接重排属性 |
| hasSuggestions | true |
元信息中同时声明了建议能力 |
| schema | [] |
规则不接受任何选项,属于纯约束规则 |
规则细节:错误与正确的代码示例
文档给出的错误示例(onSettled、onMutate、onError 乱序):
/* eslint "@tanstack/query/mutation-property-order": "warn" */
import { useMutation } from '@tanstack/react-query'
const mutation = useMutation({
mutationFn: () => Promise.resolve('success'),
onSettled: () => {
results.push('onSettled-promise')
return Promise.resolve('also-ignored') // Promise<string> (should be ignored)
},
onMutate: async () => {
results.push('onMutate-async')
await sleep(1)
return { backup: 'async-data' }
},
onError: async () => {
results.push('onError-async-start')
await sleep(1)
results.push('onError-async-end')
},
})
对应的正确写法是把三个推断敏感属性重排为 onMutate → onError → onSettled:
/* eslint "@tanstack/query/mutation-property-order": "warn" */
import { useMutation } from '@tanstack/react-query'
const mutation = useMutation({
mutationFn: () => Promise.resolve('success'),
onMutate: async () => {
results.push('onMutate-async')
await sleep(1)
return { backup: 'async-data' }
},
onError: async () => {
results.push('onError-async-start')
await sleep(1)
results.push('onError-async-end')
},
onSettled: () => {
results.push('onSettled-promise')
return Promise.resolve('also-ignored') // Promise<string> (should be ignored)
},
})
对比两个示例可以看出规则的约束边界:mutationFn 写在最前并不违规,违规点仅在 onSettled 出现在 onMutate 之前。
常量定义:检查范围与排序规则
规则的检查范围由 constants.ts 精确声明:
export const mutationFunctions = ['useMutation'] as const
export const checkedProperties = ['onMutate', 'onError', 'onSettled'] as const
export const sortRules = [[['onMutate'], ['onError', 'onSettled']]] as const
这里有一个文档未展开、但从常量可以确认的关键细节:sortRules 将属性划分为两个分组——onMutate 单独一组,onError 与 onSettled 同组。也就是说规则真正约束的是「onMutate 必须排在 onError / onSettled 之前」,而 onError 与 onSettled 二者之间的相对顺序是不检查的。这一点在测试文件 mutation-property-order.rule.test.ts 中被显式验证:生成非法排列时会跳过「onError 与 onSettled 相邻且 onMutate 不存在」的排列,并在构造期望值时注明 "since we ignore the relative order of 'onError' and 'onSettled'"。文档列出的 onMutate → onError → onSettled 应视为官方推荐的书写顺序,而非三者全序强制。
源码级实现:从导入检测到自动修复
通用工厂:createPropertyOrderRule
该规则并非独立实现,而是由通用工厂 create-property-order-rule.ts 生成——同一工厂还服务于姊妹规则 infinite-query-property-order(后者约束 queryFn 先于 getPreviousPageParam / getNextPageParam,见 constants.ts)。工厂的核心检测逻辑:
- 监听
CallExpression节点,callee 必须是标识符且命中目标函数集合(本规则仅useMutation); - 第一个参数必须是
ObjectExpression,否则跳过(例如useMutation(fn, options)的函数式签名不会被检查); - 属性少于 2 个直接跳过(无排序空间);
- 将对象属性平铺为
{ name, property }列表:key 为标识符的属性取真实名字,其余(展开元素、计算键等)统一命名为占位符_property_${index},即进入「顺序不敏感」阵营; - 调用排序工具得到期望顺序,若与原顺序一致则不报告,否则以
invalidOrder报告并附带 fixer。
排序算法:稳定的分组排序
排序由 sort-data-by-order.ts 完成。它把 orderRules 展开为有序分组序列([onMutate 组, onError/onSettled 组]),按「所属分组下标」对受约束属性做稳定排序,不命中任何分组的属性(mutationFn、gcTime、各类 spread)保持原位不动;若排序结果与输入无差异则返回 null(规则据此不报告)。稳定排序保证了同组属性之间的原有相对顺序不被打乱。
导入检测:只对来自 TanStack Query 的 useMutation 生效
规则外层包裹了 detect-react-query-imports.ts 中的 detectTanstackQueryImports:它先扫描 ImportDeclaration,只登记来源模块以 @tanstack/ 开头且以 -query 结尾(如 @tanstack/react-query、@tanstack/solid-query)的具名导入,规则只对解析到这些导入的 useMutation 标识符触发检查。从源码结构看,本地自行定义或从非 TanStack 包导入的同名函数不会被误报。
自动修复:重排属性且保留原有间隔文本
fixer(位于工厂的 fix(fixer) 回调中)将排序后的每个属性按新顺序拼接,并用 sourceCode.getText().slice(...) 取回原对象中相邻属性之间的原始文本(空白、换行、注释)填充到新序列的对应间隙,最后以 fixer.replaceTextRange 一次性替换整个属性区间。这意味着自动修复不仅重排属性,也完整保留属性之间的注释与格式——测试矩阵 mutation-property-order.rule.test.ts 中每个非法用例都断言了 output 精确等于重排后的代码,验证了这一行为。
测试矩阵:组合化验证与回归用例
测试文件用 combinate 对「目标函数 × 属性排列」做笛卡尔积,并额外把顺序不敏感属性(gcTime、...objectExpressionSpread、...callExpressionSpread、...memberCallExpressionSpread)以插值方式混入,验证:
- 三个受检属性按合法子序列出现时一律通过;
- 任意使
onMutate落在onError/onSettled之后的排列(且非「onError/onSettled 相邻而无 onMutate」的豁免情形)都会报告invalidOrder,且自动修复输出即为合法排列; - 回归用例覆盖
...mutationOptions({...})这类调用表达式展开与onMutate/onError/onSettled混排的场景——spread 被视为不透明占位符,既不会触发误报,也不会被 fixer 移动。
在项目中的接入方式
规则由 rules.ts 统一注册,发布包为 @tanstack/eslint-plugin-query(package.json 中 peer 依赖为 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0,TypeScript 为可选 peer ^5.6.0 || ^6.0.0 || ^7.0.0),因此同时兼容传统与 flat 配置。仓库内的可运行示例 examples/react/eslint-plugin-demo/eslint.config.js 展示了 flat config 下的推荐接入:
import pluginQuery from '@tanstack/eslint-plugin-query'
import tseslint from 'typescript-eslint'
export default [
...tseslint.configs.recommended,
...pluginQuery.configs['flat/recommended-strict'],
{
files: ['src/**/*.ts', 'src/**/*.tsx'],
rules: {
'@tanstack/query/exhaustive-deps': [
'error',
{
allowlist: {
variables: ['api'],
types: ['AnalyticsClient'],
},
},
],
},
},
]
引入 flat/recommended-strict 配置后,mutation-property-order 作为推荐规则默认生效(规则元信息中 recommended: 'error')。如需手动控制级别,可直接按文档示例的注释语法在规则中单独声明 @tanstack/query/mutation-property-order。该规则无选项(schema 为空数组),无需任何参数配置;配合 --fix 即可一键重排。
适用边界小结
- 仅检查
useMutation的单参数对象字面量写法;useMutation(fn, options)函数签名不在检查范围; - 仅约束
onMutate与onError/onSettled的前后关系,onError与onSettled之间相对顺序自由,其余属性与位置无关; - 展开语法(对象、调用表达式、成员调用)被当作顺序不敏感占位符,不影响判定也不参与重排;
- 只对从
@tanstack/*-query包导入的useMutation生效,避免误伤同名本地函数。
参考文件
- 规则文档:docs/eslint/mutation-property-order.md
- 规则实现:packages/eslint-plugin-query/src/rules/mutation-property-order/mutation-property-order.rule.ts、constants.ts
- 通用工厂与排序:packages/eslint-plugin-query/src/utils/create-property-order-rule.ts、packages/eslint-plugin-query/src/utils/sort-data-by-order.ts、packages/eslint-plugin-query/src/utils/detect-react-query-imports.ts
- 规则测试:packages/eslint-plugin-query/src/tests/mutation-property-order.rule.test.ts
- 接入示例:examples/react/eslint-plugin-demo/eslint.config.js
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00