深入解析 Angular Query 的 CreateBaseMutationResult:Mutation 结果对象的信号化类型骨架
导读
CreateBaseMutationResult 是 TanStack Query 的 Angular 适配包(packages/angular-query-experimental)中,用于刻画一次 mutation 完整返回结果的核心类型别名。它把 query-core 中面向框架无关的 MutationObserverResult 改造成 Angular 响应式语境下可直接消费的形状——同步的 mutate 触发器、返回 Promise 的 mutateAsync,以及后续可被收窄的状态字段。阅读本篇后,你将理解 injectMutation() 的返回值类型从何而来、四个泛型参数分别控制什么、mutate/mutateAsync 的类型差异,以及为什么该类型会作为更高层 CreateMutationResult 与状态收窄能力的构建基石。
本文的官方类型文档位于 CreateBaseMutationResult.md,其完整实现定义在 types.ts。
一、类型定位:mutation 结果类型的"基座"
在 Angular Query 的类型体系中,mutation 相关的类型不止一个,它们之间的关系是层层组合的:
| 类型 | 文件位置 | 职责 |
|---|---|---|
CreateBaseMutationResult |
types.ts | 最底层的结果形状:状态字段 + mutate/mutateAsync 调用方法 |
CreateStatusBasedMutationResult |
types.ts | 用 Extract<..., { status: TStatus }> 按 status 从结果中筛出某种具体状态下的变体 |
CreateMutationResult |
types.ts | 面向用户最终暴露的结果:在基座上叠加 BaseMutationNarrowing 收窄守卫并映射为信号 |
CreateMutationOptions |
types.ts | mutation 的配置输入类型 |
也就是说,CreateBaseMutationResult 定义了"一次 mutation 运行后你手里到底有什么",而 CreateMutationResult 只是在其之上补充了 isPending/isError/isSuccess/isIdle 等 TypeScript 类型谓词,并决定哪些字段以 Angular Signal 形式暴露。
injectMutation() 函数的官方签名返回值正是组合后的结果:
function injectMutation<TData, TError, TVariables, TOnMutateResult>(
injectMutationFn: () => CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
options?: { injector?: Injector },
): CreateMutationResult<TData, TError, TVariables, TOnMutateResult>
可参见 injectMutation 的参考文档,与类型定义文件 inject-mutation.ts 完全一致。
二、类型签名逐行拆解
类型文档给出的别名签名为:
type CreateBaseMutationResult<TData, TError, TVariables, TOnMutateResult> = Override<
MutationObserverResult<TData, TError, TVariables, TOnMutateResult>,
{
mutate: CreateMutateFunction<TData, TError, TVariables, TOnMutateResult>;
}
> & object;
仓库中 实际定义 略微展开,把 & object 声明为显式的成员:
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
>
}
这里有两个来自 query-core 的底层工具类型值得注意:
MutationObserverResult<TData, TError, TVariables, TOnMutateResult>:query-core 中 mutation 观察者对外呈现的结果,包含data、variables、error、status、isPending/isError/isSuccess/isIdle派生布尔值,以及mutate方法等,具体字段见 query-core 的 types.ts。Override<A, B>:把A中与B重叠的键用B覆盖。此处的作用是用 Angular 化的CreateMutateFunction替换掉 query-core 中框架无关的mutate字段类型,相当于"替换式的类型改造",而其余字段原样保留。
mutateAsync 是类型声明中单独声明的必选成员,与文档 Type Declaration 一节展示的一致:
mutateAsync: CreateMutateAsyncFunction<TData, TError, TVariables, TOnMutateResult>;
因此一个完整的 mutation 结果对象在类型上保证同时具备两个"开火"入口:mutate 与 mutateAsync。
三、mutate 与 mutateAsync:两种调用口径
这一对方法承担不同职责,分别由两个专门类型刻画,官方参考见 CreateMutateFunction.md 与 CreateMutateAsyncFunction.md。
1. CreateMutateFunction:同步"即发即忘"的触发函数
定义于 types.ts:
export type CreateMutateFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = (
...args: Parameters<MutateFunction<TData, TError, TVariables, TOnMutateResult>>
) => void
它直接复用 query-core MutateFunction 的参数列表,但把返回类型定为 void——这是刻意为之的:mutate 面向模板与事件回调,调用后并不等待结果、也不暴露 Promise,非常适合"按钮点击后触发、状态由 signals 驱动渲染"的响应式场景。
运行时印证在 inject-mutation.ts:实现里把底层的 observer.mutate(...) 包成同步触发、错误被 noop 吞掉的封装:
const mutateFnSignal = computed(() => {
const observer = observerSignal()
return (...args: Parameters<CreateMutateFunction<...>>) => {
observer.mutate(args[0] as TVariables, args[1]).catch(noop)
}
})
2. CreateMutateAsyncFunction:返回 Promise 的异步入口
定义于 types.ts,它没有重新发明轮子,而是直接等于 query-core 的 MutateFunction:
export type CreateMutateAsyncFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = MutateFunction<TData, TError, TVariables, TOnMutateResult>
query-core 中 MutateFunction 的真实形状(types.ts)为:
export type MutateFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> = (
...rest: MutateFunctionRest<TData, TError, TVariables, TOnMutateResult>
) => Promise<TData>
注意它返回 Promise<TData>,因此 mutateAsync 适合在服务、效果回调或组合逻辑中配合 await/try-catch 使用,能拿到成功数据或捕获失败。
值得补充的是参数元组 MutateFunctionRest(types.ts)对 TVariables 的约束逻辑:
type MutateFunctionRest<TData, TError, TVariables, TOnMutateResult> =
undefined extends TVariables
? [variables?: TVariables, options?: MutateOptions<...>]
: [variables: TVariables, options?: MutateOptions<...>]
- 当
TVariables为void或包含undefined时,variables参数可省略(例如"无参数提交"的删除操作); - 否则
variables为必填,类型安全地强制你传入变量对象。
在运行时,结果对象的 mutateAsync 直接取自 query-core 观察者原始结果中的 promise 型 mutate(见 inject-mutation.ts):
const result = resultFromSubscriber ?? resultFromInitialOptions
return {
...result,
mutate: mutateFnSignal(), // 同步即发即忘封装
mutateAsync: result.mutate, // 保留原始 Promise 版本
}
3. 两者在实践中的分工
| 特征 | mutate |
mutateAsync |
|---|---|---|
| 类型 | CreateMutateFunction |
CreateMutateAsyncFunction(即 MutateFunction) |
| 返回 | void |
Promise<TData> |
| 错误处理 | 内部 .catch(noop),错误交由观察者/信号通道处理 |
返回 rejected Promise,可用 try/catch 自行接管 |
| 典型场景 | 模板事件、响应式触发 | 组合逻辑、依赖成功后串联、需要拿返回值 |
四、四个泛型参数:默认值与含义
类型别名携带四个泛型参数,文档逐一列出,其默认值与其"基座"身份相称:
| 参数 | 默认值 | 含义 |
|---|---|---|
TData |
unknown |
mutation 成功后 data 的类型(即 mutationFn 的返回值) |
TError |
DefaultError |
失败时 error 的类型,DefaultError 即 Error 的子类约束 |
TVariables |
unknown |
提交给 mutationFn 的变量对象的类型 |
TOnMutateResult |
unknown |
onMutate 乐观更新回调返回的"上下文"类型,可在 onError/onSettled 中回滚 |
一个微妙但重要的设计:CreateBaseMutationResult 的 TVariables 默认是 unknown(因为结果对象的 variables 字段只有在真正提交后才能确定),而作为方法签名类型的 CreateMutateFunction/CreateMutateAsyncFunction 的 TVariables 默认是 void(保证"无变量"调用合法)。这体现了两者在类型上各自的关注点:结果描述已发生的事,方法签名约束将要发生的事。
五、从基座到顶层:与 CreateMutationResult 的组合关系
CreateBaseMutationResult 单独看只是静态形状,真正面向开发者的 CreateMutationResult 由 types.ts 定义:
export type CreateMutationResult<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
TState = CreateStatusBasedMutationResult<
CreateBaseMutationResult['status'], TData, TError, TVariables, TOnMutateResult
>,
> = BaseMutationNarrowing<TData, TError, TVariables, TOnMutateResult> &
MapToSignals<OmitKeyof<TState, keyof BaseMutationNarrowing, 'safely'>>
逐段解读:
CreateBaseMutationResult['status']取出基座的status键作为约束,再由CreateStatusBasedMutationResult通过Extract选出与当前status匹配的精确状态子集——这是 Angular Query 做 discriminated-union 收窄的关键(官方文档见 CreateMutationResult.md)。BaseMutationNarrowing(types.ts)定义了四个 TypeScript 类型谓词isSuccess、isError、isPending、isIdle,每个都形如(this: CreateMutationResult<...>) => this is CreateMutationResult<..., CreateStatusBasedMutationResult<'success', ...>>,据此在if分支内自动收窄出对应状态的类型。MapToSignals<T>来自 signal-proxy.ts:它把所有非函数字段映射为 Angularexport type MapToSignals<T> = { [K in keyof T]: T[K] extends Function ? T[K] : Signal<T[K]> }Signal(读取时用mutation.data()),而函数字段原样保留(mutation.isPending()直接调用)。配合signalProxy代理(signal-proxy.ts),运行时把 mutation 结果中的每个字段包装成computed信号。OmitKeyof<TState, keyof BaseMutationNarrowing, 'safely'>从状态中剔除四个收窄守卫键,避免与顶层谓词重复冲突。
类型层面有编译期测试兜底,例如 mutation-options.test-d.ts 对 CreateMutationResult<string, DefaultError, void, unknown> 进行了类型断言,确保泛型接线正确。
六、实战视角:injectMutation 的结果如何被消费
综合以上类型设计,在组件中注入 mutation 时:
import { injectMutation } from '@tanstack/angular-query-experimental'
@Component({ /* ... */ })
export class TodosComponent {
private readonly todosService = inject(TodosService)
// 返回类型即 CreateMutationResult<Todo, DefaultError, string, unknown>
readonly addTodo = injectMutation(() => ({
mutationFn: (title: string) => this.todosService.add(title),
onSuccess: () => this.todosService.invalidateList(),
}))
}
在模板或代码中读取结果时,遵循"函数字段直接调用、普通字段通过信号读取"的约定:
- 状态收窄:
this.addTodo.isPending()、this.addTodo.isSuccess()、this.addTodo.isError()、this.addTodo.isIdle()——它们是被保留的类型谓词函数; - 数据与错误:
this.addTodo.data()、this.addTodo.error()、this.addTodo.variables()、this.addTodo.status()——它们是被映射为Signal的字段; - 触发提交:在事件处理中调用
this.addTodo.mutate(newTitle)(同步、即发即忘),或在需要串联副作用时await this.addTodo.mutateAsync(newTitle)。
这种"一个 mutation 结果里既有信号化状态、又有两种触发方式"的类型契约,正是由 CreateBaseMutationResult 这一基座类型通过 Override、& 交叉与信号映射层层搭建出来的。理解它,也就理解了 injectMutation() 返回值在模板中为何"既能读取、又能调用"。
七、小结
CreateBaseMutationResult是 Angular Query mutation 结果类型的底层骨架,定义于 types.ts,文档见 CreateBaseMutationResult.md。- 它用
Override把 query-core 的MutationObserverResult中的mutate替换为 Angular 化同步版本,并声明了 promise 型的mutateAsync。 - 四个泛型
TData/TError/TVariables/TOnMutateResult分别控制数据、错误、提交变量与乐观更新上下文,默认值依次为unknown/DefaultError/unknown/unknown。 - 它是
CreateStatusBasedMutationResult、CreateMutationResult与BaseMutationNarrowing收窄体系的输入来源;运行时由injectMutation经signalProxy将其实例化为信号化对象。 - 若要继续追踪兄弟类型,可对照同目录下的 CreateMutationResult.md、CreateMutateFunction.md 与 CreateMutateAsyncFunction.md。
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