Angular Query 中的 CreateMutateFunction 类型:理解 mutation 触发函数的设计与推断规则
本篇文章面向在 Angular 应用中使用 TanStack Angular Query 的开发者,围绕 packages/angular-query-experimental/src/types.ts 中定义的 CreateMutateFunction 类型别名展开。它出现在 injectMutation 返回的 mutation 结果对象上,是触发服务端副作用(创建、更新、删除等)的统一入口。读完本文,你将掌握 CreateMutateFunction 的完整类型签名、四个泛型参数的语义、与底层 MutateFunction/CreateMutateAsyncFunction 的关系,以及在 Angular 组件中正确使用与类型推断的实战技巧。
一、什么是 CreateMutateFunction
在 Angular Query 的官方类型参考文档(CreateMutateFunction.md)中,该类型被定义为:
type CreateMutateFunction<TData, TError, TVariables, TOnMutateResult> = (...args) => void;
它是一个函数的类型描述,接收任意参数(具体参数由 Parameters<...> 推导而来),并返回 void。它的实际定义位于仓库源码 packages/angular-query-experimental/src/types.ts#L136-L145:
export type CreateMutateFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = (
...args: Parameters<
MutateFunction<TData, TError, TVariables, TOnMutateResult>
>
) => void
理解这个类型的关键在于两点:
- 参数列表不是手写的,而是通过
Parameters<MutateFunction<...>>从 TanStack Query 核心包的类型中自动提取,从而保证与底层实现严格同步; - 返回值固定为
void,也就是说它代表一种"触发后不等待结果"的命令式调用——这正是injectMutation返回对象上mutate方法的形态。
CreateMutateFunction 并不是孤立存在的:它出现在 types.ts#L154-L169 中 CreateBaseMutationResult 的 mutate 字段上,并通过 CreateMutationResult(types.ts#L260-L273)最终成为 injectMutation() 的返回值类型。官方类型参考中的定义在 injectMutation.md。
二、四个类型参数(Type Parameters)逐一拆解
CreateMutateFunction 带四个泛型参数,官方类型参考文档为它们各自标注了默认值:
TData
- 默认值:
unknown - 含义:mutation 函数成功解析后返回的数据类型。也就是传给
injectMutation(() => ({ mutationFn: ... }))中mutationFn的返回数据类型。由于默认是unknown,TS 无法在未显式标注时替你校验成功回调里的data,通常建议让 TypeScript 从mutationFn返回类型推断出来。
TError
- 默认值:
DefaultError - 含义:mutation 失败时的错误类型。
DefaultError来自 TanStack Query 核心包(在 packages/query-core/src 中导出),默认即Error。当你使用自定义错误体系(例如 axios 封装后的业务错误类型)时,这个参数会被你的injectMutation泛型自动绑定。
TVariables
- 默认值:
void - 含义:触发 mutation 时需要传入的变量类型,即调用
mutation.mutate(variables)时的实参类型。默认值void意味着"不需要变量";一旦你的mutationFn声明了形参(例如(todoId: number) => ...),TS 就会将TVariables推断为number。
TOnMutateResult
- 默认值:
unknown - 含义:mutation 中
onMutate乐观更新回调的返回值类型。它与"乐观更新/回滚"场景强相关:onMutate可以返回一个上下文对象(如回滚所需的快照),这个类型会在onError、onSuccess、onSettled回调的第三参中体现。可参见 optimistic-updates.md。
三、与核心类型 MutateFunction 的继承关系
CreateMutateFunction 的参数不是凭空而来,而是取自 TanStack Query 核心包中的 packages/query-core/src/types.ts#L1194-L1201:
export type MutateFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = (
...rest: MutateFunctionRest<TData, TError, TVariables, TOnMutateResult>
) => Promise<TData>
而 MutateFunctionRest(types.ts#L1179-L1192)则是一个条件类型,它决定了参数是"必需"还是"可选":
export type MutateFunctionRest<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = undefined extends TVariables
? [
variables?: TVariables,
options?: MutateOptions<TData, TError, TVariables, TOnMutateResult>,
]
: [
variables: TVariables,
options?: MutateOptions<TData, TError, TVariables, TOnMutateResult>,
]
由此可以得出一个直接影响日常编码的规则:
- 当
TVariables可能为undefined时(例如未声明变量、或声明为可选),调用mutate()可以省略变量参数; - 当
TVariables是明确类型时(例如number),variables参数是必填的,TypeScript 会在编译期强制你传入变量,避免"忘了传参却调用了带参 mutation"这类运行时错误。
第二个参数 options 始终可选,其类型 MutateOptions(types.ts#L1152-L1177)允许你在单次调用时挂载 onSuccess、onError、onSettled 回调:
export interface MutateOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> {
onSuccess?: (data, variables, onMutateResult, context) => void
onError?: (error, variables, onMutateResult, context) => void
onSettled?: (data, error, variables, onMutateResult, context) => void
}
这些逐次调用传入的回调会与 injectMutation 选项中定义的回调一起被触发(先执行全局选项回调,再执行本次调用回调),这在 mutations.md 的"promise 与回调执行顺序"一节有详细示例。
四、CreateMutateFunction 与 CreateMutateAsyncFunction 的本质区别
CreateMutateFunction 的孪生类型是 CreateMutateAsyncFunction.md 中记录的 CreateMutateAsyncFunction。二者在源码中紧挨着定义(types.ts#L136-L152):
export type CreateMutateFunction<...> = (
...args: Parameters<MutateFunction<TData, TError, TVariables, TOnMutateResult>>
) => void // 不返回 Promise
export type CreateMutateAsyncFunction<...> =
MutateFunction<TData, TError, TVariables, TOnMutateResult> // 返回 Promise<TData>
二者唯一的差别是返回值:
CreateMutateFunction→void,对应 mutation 结果对象上的mutate方法,采用"触发即忘"(fire-and-forget)语义,调用后不直接拿到 Promise,适合在模板事件(如按钮点击)中绑定;CreateMutateAsyncFunction→Promise<TData>,对应mutateAsync方法,可以用await等待结果并用try/catch/finally处理流程,适合需要在调用处串联后续逻辑的场景(见 mutations.md 中 mutateAsync 的示例)。
CreateBaseMutationResult(types.ts#L154-L169)正是通过 Override<..., { mutate: CreateMutateFunction<...> }> 显式把核心结果类型中的 mutate 替换成 CreateMutateFunction 形态,并额外补充 mutateAsync 字段:
export type CreateBaseMutationResult<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> = Override<
MutationObserverResult<TData, TError, TVariables, TOnMutateResult>,
{ mutate: CreateMutateFunction<TData, TError, TVariables, TOnMutateResult> }
> & {
mutateAsync: CreateMutateAsyncFunction<TData, TError, TVariables, TOnMutateResult>
}
五、源码级印证:CreateMutateFunction 在 injectMutation 中如何被构造
仅看类型定义容易停留在抽象层面。回到实现,inject-mutation.ts#L85-L96 中有一段直接构造 CreateMutateFunction 的代码:
const mutateFnSignal = computed<
CreateMutateFunction<TData, TError, TVariables, TOnMutateResult>
>(() => {
const observer = observerSignal()
return (
...args: Parameters<
CreateMutateFunction<TData, TError, TVariables, TOnMutateResult>
>
) => {
observer.mutate(args[0] as TVariables, args[1]).catch(noop)
}
})
这里印证了三件事:
- 函数签名确实通过
Parameters<...>生成,与类型定义一一对应:args[0]是变量,args[1]是本次调用的MutateOptions; - 返回值确实是
void:内部调用底层observer.mutate(...)返回的 Promise 后直接.catch(noop)丢弃,调用方无法(也不需要)拿到该 Promise; - 错误不会静默丢失:由于
mutate的结果被吞掉,运行时错误通过 mutation 订阅机制传播——inject-mutation.ts#L152-L158 中,当state.isError且满足shouldThrowError(observer.options.throwOnError, [state.error])时,会通过ngZone.onError.emit(state.error)抛出并中断;否则错误体现在mutation.error()信号中,由模板负责展示。此外,mutation 处于 pending 期间还会通过 AngularPENDING_TASKS注册等待任务,避免 SSR/水合时提前完成,相关常量定义在 pending-tasks-compat.ts。
由于结果对象由 signalProxy(signal-proxy.ts)包装,injectMutation 最终返回的 CreateMutationResult 上的属性以 signal 形式访问,因此模板中通常写作 mutation.isPending()、mutation.error()、mutation.isError() 等;而 mutate 本身是纯函数,直接以 mutation.mutate(...) 调用。
六、实战:在 Angular 组件中触发 mutation
下面这段来自 mutations.md 的示例展示了 CreateMutateFunction 所描述的 mutate 在实际组件模板与类中的典型用法:
@Component({
template: `
<div>
@if (mutation.isPending()) {
<span>Adding todo...</span>
} @else if (mutation.isError()) {
<div>An error occurred: {{ mutation.error()?.message }}</div>
} @else if (mutation.isSuccess()) {
<div>Todo added!</div>
}
<button (click)="mutation.mutate(1)">Create Todo</button>
</div>
`,
})
export class TodosComponent {
todoService = inject(TodoService)
mutation = injectMutation(() => ({
mutationFn: (todoId: number) =>
lastValueFrom(this.todoService.create(todoId)),
}))
}
这里 mutate(1) 的入参 1 之所以能被 TypeScript 接受,正是因为它满足 CreateMutateFunction<..., number, ...> 的推导结果:mutationFn 声明的 todoId: number 让 TVariables 被推断为 number,于是 MutateFunctionRest 走"变量必填"分支。
需要显式校验返回值/变量类型的场景,可以使用 mutate 的第二个参数注入本次调用专属回调:
mutation = injectMutation(() => ({
mutationFn: addTodo,
onSuccess: (data, variables, onMutateResult, context) => {
// I will fire first
},
}))
this.mutation.mutate(todo, {
onSuccess: (data, variables, onMutateResult, context) => {
// I will fire second!
},
onError: (error, variables, onMutateResult, context) => {
// 本次调用专属的错误处理
},
})
如果需要在 mutation 完成后立即用返回数据驱动下一步逻辑,则切换到 mutateAsync(CreateMutateAsyncFunction 的形态):
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
关于乐观更新时 onMutate 返回上下文、以及 mutation 触发后使相关查询失效的完整链路,可继续阅读 optimistic-updates.md 与 invalidations-from-mutations.md。
七、类型推断建议与常见坑
- 优先让泛型自动推断:
injectMutation的泛型顺序为<TData, TError, TVariables, TOnMutateResult>(见 inject-mutation.ts#L45-L58),多数情况下你只需写好mutationFn,四个泛型即可全部正确推导,无需手工标注; TData默认unknown时的防护:若mutationFn返回Promise<unknown>,onSuccess的data参数也会是unknown,模板中使用前应先做类型收窄;TVariables的必填/可选由类型本身决定:不要把"是否传变量"寄托在记忆上,TS 会在你遗漏必填变量时报编译错误,这是MutateFunctionRest条件类型带来的编译期保障;- 不要在必须拿到结果的场景用
mutate:CreateMutateFunction返回void,如果你需要await结果或捕获本次调用的 reject,请使用mutateAsync,否则只能依赖onError/onSettled回调; - mutation 结果对象的
status/error等字段是 signals:这些来自CreateMutationResult经MapToSignals变换后的形态(相关类型定义见 CreateMutationResult.md),在模板与effect中应按信号方式读取。
相关参考
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 StartedRust0627
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