TanStack Query ESLint 插件:infinite-query-property-order 规则详解——无限滚动查询的属性顺序与类型推断
对于在 React、Solid、Svelte、Vue 等框架中使用 TanStack Query 做无限滚动(infinite query)的开发者来说,infinite-query-property-order 是 @tanstack/eslint-plugin-query 插件中一条针对类型推断敏感的代码风格规则:它要求 useInfiniteQuery、useSuspenseInfiniteQuery 与 infiniteQueryOptions 这三个函数在接收对象参数时,把 queryFn、getPreviousPageParam、getNextPageParam 按固定顺序书写,否则 TypeScript 无法正确推断页面数据类型,进而导致编译错误或类型退化。读完全文,你将理解该规则的触发条件、正确写法,以及插件在源码层面如何实现检测与自动修复。
规则核心:哪些属性顺序敏感
官方规则文档 docs/eslint/infinite-query-property-order.md 指出,对于以下三个函数,传入对象的属性顺序会影响 TypeScript 的类型推断:
useInfiniteQueryuseSuspenseInfiniteQueryinfiniteQueryOptions
正确的属性顺序为:
queryFngetPreviousPageParamgetNextPageParam
其余属性(如 queryKey、initialPageParam、maxPages 等)对顺序不敏感,因为它们不参与这条类型推断链。
这一点可以从插件的规则常量文件得到印证。constants.ts 中定义了:
export const infiniteQueryFunctions = [
'infiniteQueryOptions',
'useInfiniteQuery',
'useSuspenseInfiniteQuery',
] as const
export const checkedProperties = [
'queryFn',
'getPreviousPageParam',
'getNextPageParam',
] as const
export const sortRules = [
[['queryFn'], ['getPreviousPageParam', 'getNextPageParam']],
] as const
sortRules 的结构是一组 [前置集合, 后置集合] 的有序约束:queryFn 必须先出现,getPreviousPageParam 和 getNextPageParam 必须在它之后。这两个页面参数读取函数内部声明的回调参数(如 firstPage、lastPage)类型依赖于 queryFn 返回的数据结构,若写在 queryFn 之前,TypeScript 在推断泛型时缺少上下文,就会导致类型推导失败。
规则示例:错误与正确写法
规则文档给出的错误示例——getNextPageParam 被提前到了 queryFn 之前:
/* eslint "@tanstack/query/infinite-query-property-order": "warn" */
import { useInfiniteQuery } from '@tanstack/react-query'
const query = useInfiniteQuery({
queryKey: ['projects'],
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
queryFn: async ({ pageParam }) => {
const response = await fetch(`/api/projects?cursor=${pageParam}`)
return await response.json()
},
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
maxPages: 3,
})
对应的正确写法——三个敏感属性按 queryFn → getPreviousPageParam → getNextPageParam 顺序排列,其余属性位置不变:
/* eslint "@tanstack/query/infinite-query-property-order": "warn" */
import { useInfiniteQuery } from '@tanstack/react-query'
const query = useInfiniteQuery({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
const response = await fetch(`/api/projects?cursor=${pageParam}`)
return await response.json()
},
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
maxPages: 3,
})
该规则在文档的 Attributes 一节中标记为 Recommended(推荐启用) 且 Fixable(可自动修复),即开启 eslint --fix 即可自动重排属性。
规则元数据:报错级别与修复能力
规则本体定义于 infinite-query-property-order.rule.ts:
export const rule = createPropertyOrderRule<
InfiniteQueryFunctions,
InfiniteQueryProperties
>(
{
name,
meta: {
type: 'problem',
docs: {
description:
'Ensure correct order of inference sensitive properties for infinite queries',
recommended: 'error',
},
messages: {
invalidOrder: 'Invalid order of properties for `{{function}}`.',
},
schema: [],
hasSuggestions: true,
fixable: 'code',
},
defaultOptions: [],
},
infiniteQueryFunctions,
sortRules,
)
从 meta 配置可以看到几个关键事实:
type: 'problem':被归类为真实问题而非单纯风格建议,因为顺序错误会直接造成类型推断失败;recommended: 'error':在插件的推荐规则集中默认以error级别启用;fixable: 'code'且hasSuggestions: true:支持--fix自动修复;- 报错信息模板为
Invalid order of properties for \{{function}}`.,会填入具体函数名(如useInfiniteQuery`); schema: []:该规则不接受任何配置选项,开关即生效。
该规则通过 rules.ts 注册进插件的规则表,与 mutation-property-order、no-unstable-deps 等规则并列导出。
源码剖析:检测与自动修复是如何实现的
共享的规则工厂
该规则并未从零实现,而是复用了通用工厂函数 createPropertyOrderRule,它接收“目标函数列表”和“顺序约束规则”两个参数,生成完整的 ESLint 规则。其核心检测逻辑在 CallExpression 监听器中:
- 仅当调用目标是普通标识符(
Identifier)且函数名命中目标列表(useInfiniteQuery等)时才继续; - 第一个参数必须是对象字面量(
ObjectExpression),否则跳过; - 对象属性少于 2 个时直接跳过(无需排序);
- 将每个属性映射为
{ name, property },其中非标识符键(如计算属性)会被赋予占位名_property_${index},从而不参与排序; - 用 sortDataByOrder 计算重排结果,若返回非空值说明存在顺序错误,随即调用
context.report上报invalidOrder错误。
这一流程与文档描述完全一致:只有三个敏感属性违反顺序约束时才会报错,其他属性任意摆放都不会触发。
排序算法:只重排敏感属性,保持其余属性原地不动
sort-data-by-order.ts 实现了稳定的相对排序:
- 先把排序规则展开为若干“子集”(对本规则即
[queryFn]与[getPreviousPageParam, getNextPageParam]两个子集,前者优先); - 只筛选出属于这些子集的属性参与比较排序,同属一个子集的属性保持原有相对顺序;
- 不在任何子集中的属性(如
queryKey、initialPageParam、maxPages)位置完全不变; - 若最终没有任何敏感属性被移动(
wasResorted为false),返回null,规则因此不报错。
这解释了文档中“其余属性对顺序不敏感”的原因:排序算法在设计上就只移动三个受检属性,其余属性在修复后也保持原位。
自动修复:保留属性间的原始文本
工厂函数中的 fix(fixer) 回调负责生成修复文本:它按排序后的属性顺序,逐个取属性的源码文本拼接,并夹入相邻属性之间原有的间隔文本(注释、空行等):
let textBetweenProperties = ''
if (index < allProperties.length - 1) {
textBetweenProperties = sourceCode
.getText()
.slice(allProperties[index]!.range[1], allProperties[index + 1]!.range[0])
}
最终通过 fixer.replaceTextRange 将首个属性到最后一个属性的整段文本替换为重排后的文本。也就是说,eslint --fix 只重排敏感属性的位置,属性之间的手写格式内容会被原样搬运。
仅在 TanStack Query 导入上生效
一个重要的边界条件来自 detect-react-query-imports.ts。所有插件规则都被 detectTanstackQueryImports 包装,只有当函数确实是从 @tanstack/ 开头、以 -query 结尾的包(如 @tanstack/react-query)中具名导入时,检测才生效:
node.source.value.startsWith('@tanstack/') &&
node.source.value.endsWith('-query')
因此,如果项目里有一个自定义的同名 useInfiniteQuery 工具函数,该规则不会误报;反过来,从 @tanstack/react-query 导入的调用则会被完整检查。该包装器通过收集 ImportDeclaration 中所有导入说明符、再在规则执行前判断标识符是否来自上述包名来实现精确匹配。
测试验证
仓库提供了对应的单元测试文件 infinite-query-property-order.rule.test.ts,位于插件的 tests 目录 下,与文档中的正/误示例共同构成该规则行为的回归保障;排序算法本身另有 sort-data-by-order.utils.test.ts 覆盖,可用于验证“只重排敏感属性、其余属性保持原位”的行为。
实践要点小结
- 何时触发:对象参数形式调用
useInfiniteQuery/useSuspenseInfiniteQuery/infiniteQueryOptions,且第一个参数为对象字面量、含两个以上属性时; - 必须遵守的顺序:
queryFn→getPreviousPageParam→getNextPageParam;queryKey、initialPageParam、maxPages等其他属性可随意放置; - 报错与修复:报错信息会指明具体函数名(
Invalid order of properties for \useInfiniteQuery`.),执行eslint --fix` 可自动重排,且仅移动敏感属性; - 推荐级别:插件推荐集中默认以
error级别启用(文档标注 Recommended); - 误报防护:规则只检查从
@tanstack/*-query包导入的函数,自定义同名函数不受影响。
把该规则加入团队的 ESLint 配置后,属性顺序错误会在提交前被静态发现并可一键修复,从而避免无限滚动查询因属性书写顺序不同而在不同文件中出现类型推断失败的问题。
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 StartedRust0623
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