首页
/ Angular Query 中的 CreateMutateFunction 类型:理解 mutation 触发函数的设计与推断规则

Angular Query 中的 CreateMutateFunction 类型:理解 mutation 触发函数的设计与推断规则

2026-09-07 16:53:27作者:尤峻淳Whitney

本篇文章面向在 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

理解这个类型的关键在于两点:

  1. 参数列表不是手写的,而是通过 Parameters<MutateFunction<...>> 从 TanStack Query 核心包的类型中自动提取,从而保证与底层实现严格同步;
  2. 返回值固定为 void,也就是说它代表一种"触发后不等待结果"的命令式调用——这正是 injectMutation 返回对象上 mutate 方法的形态。

CreateMutateFunction 并不是孤立存在的:它出现在 types.ts#L154-L169CreateBaseMutationResultmutate 字段上,并通过 CreateMutationResulttypes.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 可以返回一个上下文对象(如回滚所需的快照),这个类型会在 onErroronSuccessonSettled 回调的第三参中体现。可参见 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>

MutateFunctionResttypes.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 始终可选,其类型 MutateOptionstypes.ts#L1152-L1177)允许你在单次调用时挂载 onSuccessonErroronSettled 回调:

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 与回调执行顺序"一节有详细示例。

四、CreateMutateFunctionCreateMutateAsyncFunction 的本质区别

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>

二者唯一的差别是返回值:

  • CreateMutateFunctionvoid,对应 mutation 结果对象上的 mutate 方法,采用"触发即忘"(fire-and-forget)语义,调用后不直接拿到 Promise,适合在模板事件(如按钮点击)中绑定;
  • CreateMutateAsyncFunctionPromise<TData>,对应 mutateAsync 方法,可以用 await 等待结果并用 try/catch/finally 处理流程,适合需要在调用处串联后续逻辑的场景(见 mutations.md 中 mutateAsync 的示例)。

CreateBaseMutationResulttypes.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>
}

五、源码级印证:CreateMutateFunctioninjectMutation 中如何被构造

仅看类型定义容易停留在抽象层面。回到实现,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)
  }
})

这里印证了三件事:

  1. 函数签名确实通过 Parameters<...> 生成,与类型定义一一对应:args[0] 是变量,args[1] 是本次调用的 MutateOptions
  2. 返回值确实是 void:内部调用底层 observer.mutate(...) 返回的 Promise 后直接 .catch(noop) 丢弃,调用方无法(也不需要)拿到该 Promise;
  3. 错误不会静默丢失:由于 mutate 的结果被吞掉,运行时错误通过 mutation 订阅机制传播——inject-mutation.ts#L152-L158 中,当 state.isError 且满足 shouldThrowError(observer.options.throwOnError, [state.error]) 时,会通过 ngZone.onError.emit(state.error) 抛出并中断;否则错误体现在 mutation.error() 信号中,由模板负责展示。此外,mutation 处于 pending 期间还会通过 Angular PENDING_TASKS 注册等待任务,避免 SSR/水合时提前完成,相关常量定义在 pending-tasks-compat.ts

由于结果对象由 signalProxysignal-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: numberTVariables 被推断为 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 完成后立即用返回数据驱动下一步逻辑,则切换到 mutateAsyncCreateMutateAsyncFunction 的形态):

try {
  const todo = await mutation.mutateAsync(todo)
  console.log(todo)
} catch (error) {
  console.error(error)
} finally {
  console.log('done')
}

关于乐观更新时 onMutate 返回上下文、以及 mutation 触发后使相关查询失效的完整链路,可继续阅读 optimistic-updates.mdinvalidations-from-mutations.md

七、类型推断建议与常见坑

  1. 优先让泛型自动推断injectMutation 的泛型顺序为 <TData, TError, TVariables, TOnMutateResult>(见 inject-mutation.ts#L45-L58),多数情况下你只需写好 mutationFn,四个泛型即可全部正确推导,无需手工标注;
  2. TData 默认 unknown 时的防护:若 mutationFn 返回 Promise<unknown>onSuccessdata 参数也会是 unknown,模板中使用前应先做类型收窄;
  3. TVariables 的必填/可选由类型本身决定:不要把"是否传变量"寄托在记忆上,TS 会在你遗漏必填变量时报编译错误,这是 MutateFunctionRest 条件类型带来的编译期保障;
  4. 不要在必须拿到结果的场景用 mutateCreateMutateFunction 返回 void,如果你需要 await 结果或捕获本次调用的 reject,请使用 mutateAsync,否则只能依赖 onError/onSettled 回调;
  5. mutation 结果对象的 status/error 等字段是 signals:这些来自 CreateMutationResultMapToSignals 变换后的形态(相关类型定义见 CreateMutationResult.md),在模板与 effect 中应按信号方式读取。

相关参考

登录后查看全文
热门项目推荐
相关项目推荐