TanStack Query Angular 版:用 mutationOptions 以类型安全的方式共享 Mutation 配置
在 Angular 应用中管理异步服务端状态时,mutation(写操作)的配置往往需要在服务层集中定义、在多个组件中复用。Angular Query(@tanstack/angular-query-experimental)提供的 mutationOptions 辅助函数正是为此设计的:它在运行时是一个纯粹的“透传”函数,其全部价值在于 TypeScript 类型层面——让你把 mutation 的所有可选项定义在一处,并在所有使用点上获得完整的类型推断与类型安全。读完本文,你将掌握如何在 Angular 服务中定义可复用的 mutation 选项、mutationOptions 的函数重载机制与底层类型构成,以及它与 injectMutation 的协作方式。
mutationOptions 是什么:运行时透传,类型层增强
官方文档 mutation-options.md 对它的描述非常直接:
One of the best ways to share mutation options between multiple places, is to use the
mutationOptionshelper. At runtime, this helper just returns whatever you pass into it, but it has a lot of advantages when using it with TypeScript.
也就是说,mutationOptions 不改变任何运行时行为,它存在的意义是让“把 options 抽到独立函数/服务中”这件事不丢失类型信息。如果没有这个辅助函数,当你把 options 提取到另一个函数里返回时,TypeScript 就无法把 mutationFn 的返回类型自动传播到回调参数(如 onSuccess 的 data)上,类型推断链路就断了。
源码实现:一个函数,三个签名
从源码看,mutationOptions 的实现极其简单,位于 mutation-options.ts:
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
options: CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
): CreateMutationOptions<TData, TError, TVariables, TOnMutateResult> {
return options
}
实现体只有一行 return options(见 mutation-options.ts#L103-L112),完全印证了文档中“运行时原样返回入参”的说法。
但真正的关键在它上面的两个函数重载(mutation-options.ts#L39-L66):
// 重载一:要求 mutationKey 必须存在
export function mutationOptions<TData, TError, TVariables, TOnMutateResult>(
options: WithRequired<
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
'mutationKey'
>,
): WithRequired<
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
'mutationKey'
>
// 重载二:允许省略 mutationKey(返回类型也相应 Omit 掉 mutationKey)
export function mutationOptions<TData, TError, TVariables, TOnMutateResult>(
options: Omit<
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
'mutationKey'
>,
): Omit<
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
'mutationKey'
>
这组重载带来两个实际的类型语义:
- 带
mutationKey的分支:返回类型通过WithRequired把mutationKey固化为必填,后续消费该 options 的 API(如queryClient.mutate、setMutationDefaults等基于 key 的接口)可以利用 key 类型做进一步推断。 - 不带
mutationKey的分支:当你只想共享mutationFn与回调、而 key 通过queryClient.setMutationDefaults(['addTodo'], ...)注册默认值时,走的是这个重载,返回类型中不含mutationKey,与 mutations 指南 中setMutationDefaults的模式配套使用。
四个泛型参数 TData、TError、TVariables、TOnMutateResult 的默认值分别是 unknown、DefaultError、void、unknown,与 query-core 中 MutationObserverOptions 的约定一致。
CreateMutationOptions 的类型构成
mutationOptions 收发的选项类型是 CreateMutationOptions,定义在 types.ts#L126-L134:
export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> extends OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>,
'_defaulted'
> {}
它本质上就是 query-core 的 MutationObserverOptions 去掉内部字段 _defaulted。这意味着你写进 mutationOptions 的配置项与直接传给 injectMutation 的完全一致:mutationFn、mutationKey、onMutate、onSuccess、onError、onSettled、retry、throwOnError 等(完整回调签名与执行顺序参见 mutations 指南)。该函数由包入口 index.ts#L16 导出,与 queryOptions(query-options.md)对称,是同一套“options 提取”设计在 mutation 侧的对应物。
完整实战:在服务中定义 mutationOptions
官方文档给出的示例是一个注入 HttpClient 的服务,把“更新帖子”mutation 的全部配置集中在一处:
export class QueriesService {
private http = inject(HttpClient)
updatePost(id: number) {
return mutationOptions({
mutationFn: (post: Post) => Promise.resolve(post),
mutationKey: ['updatePost', id],
onSuccess: (newPost) => {
// ^? newPost: Post
this.queryClient.setQueryData(['posts', id], newPost)
},
})
}
}
这里体现了两个核心收益:
- 回调参数自动推断:
onSuccess的newPost被推断为Post(即mutationFn的返回类型),无需手写泛型参数。文档中特意标注^? newPost: Post来提示你在编辑器里把光标悬停验证。 - mutation 结果与 query 缓存联动:
onSuccess中直接调用queryClient.setQueryData(['posts', id], newPost),把 mutation 的成功结果写入对应 query 缓存,实现无感知的数据更新。
mutation-options.ts 文件头部 JSDoc 中的示例(mutation-options.ts#L7-L35)还展示了组件侧的配套用法,完整链路如下:
export class QueriesService {
private http = inject(HttpClient)
private queryClient = inject(QueryClient)
updatePost(id: number) {
return mutationOptions({
mutationFn: (post: Post) => Promise.resolve(post),
mutationKey: ['updatePost', id],
onSuccess: (newPost) => {
this.queryClient.setQueryData(['posts', id], newPost)
},
})
}
}
class ComponentOrService {
queries = inject(QueriesService)
id = signal(0)
// 注意:injectMutation 的入参是“返回 options 的函数”,
// 函数内使用 signal 表达式可以让 options 保持响应式
mutation = injectMutation(() => this.queries.updatePost(this.id()))
save() {
this.mutation.mutate({ title: 'New Title' })
}
}
注意 injectMutation(() => this.queries.updatePost(this.id())) 这行:injectMutation 的入参类型是 () => CreateMutationOptions<...>,接受一个惰性求值的函数。因此你在函数体里可以引用 signal(如 this.id()),id 变化时 options 会随之重新计算。仓库中类似的“服务集中管理 options”模式可参考 query-options-from-a-service 示例(该示例以 queryOptions 演示同一思路)。
与 injectMutation 的协作机制
结合 inject-mutation.ts 的源码,可以看清 mutationOptions 的返回值是如何被消费的(inject-mutation.ts#L45-L83):
export function injectMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
injectMutationFn: () => CreateMutationOptions<
TData, TError, TVariables, TOnMutateResult
>,
options?: InjectMutationOptions,
): CreateMutationResult<TData, TError, TVariables, TOnMutateResult> {
// ...
const optionsSignal = computed(injectMutationFn)
const observerSignal = (() => {
let instance: MutationObserver<...> | null = null
return computed(() => {
return (instance ||= new MutationObserver(queryClient, optionsSignal()))
})
})()
// ...
}
从源码结构看,injectMutation 把 options 函数包进 computed,从而允许在其中使用 signal 表达式并保持响应性;options 变化时通过 observer.setOptions(...) 同步给底层的 MutationObserver(query-core 提供)。mutationOptions 本身不参与这个过程,它只在“定义侧”把类型锁定住,保证这个链路(mutationFn 返回类型 → onSuccess 参数 → 组件中 mutation.data() 的类型)不丢失。
返回的结果对象经过 signalProxy(resultSignal) 包装,模板中可直接以信号方式访问:mutation.isPending()、mutation.isError()、mutation.error() 等,mutate 用于即发即忘地触发,mutateAsync 则返回 Promise(mutations 指南 中有完整模板示例与 mutateAsync 用法)。
使用要点与适用边界
- 何时必须用
mutationOptions:只要你把 mutation options 从injectMutation内联写法提取到独立函数或服务方法中,就必须包一层mutationOptions才能保留类型推断——这正是它与 TypeScript 指南中 “Typing Mutation Options” 一节 所讲queryOptions的对应关系。 mutationKey两种姿势:带 key 定义时走WithRequired重载,key 类型可用于queryClient.mutate等接口推断;不带 key 时用setMutationDefaults注册全局默认(回调签名、重试、乐观更新回滚等写法见 mutations 指南),此时injectMutation只需传mutationKey即可命中默认配置。- 响应式 options:
injectMutation的入参是函数,配合 signal 可在运行时切换mutationFn、mutationKey等字段(如上例this.queries.updatePost(this.id())),mutationOptions只是静态的类型包装,不影响这一能力。 - 乐观更新场景:
mutationOptions定义处可以完整承载onMutate/onSuccess/onError三段式乐观更新逻辑,组件侧无需重复编写;仓库的 optimistic-updates 示例 展示了 Angular 侧该模式的完整形态。 - 注意包名与状态:当前 Angular 集成的包名是
@tanstack/angular-query-experimental(见 packages/angular-query-experimental 目录与 index.ts 的导出),处于 experimental 阶段;mutationOptions的运行时行为(原样返回)与类型构成(CreateMutationOptions继承自 query-core 的MutationObserverOptions)均可以直接从上述源码验证。
小结
mutationOptions 是 Angular Query 中“options 集中管理”模式在 mutation 侧的组成部分:运行时零成本(return options),类型层收益显著——单一位置定义 mutationFn/mutationKey/回调,所有引用点获得参数推断与类型安全。它的两个重载区分“带 key”与“不带 key(配 setMutationDefaults)”两种共享方式,返回值可直接作为 injectMutation 惰性 options 函数(injectMutation(() => service.updatePost(id)))的产物参与响应式更新。配合 queryOptions 指南、mutations 指南 与 TypeScript 指南,即可覆盖 Angular 应用中从读(query)到写(mutation)的完整类型安全链路。
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 StartedRust0625
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