首页
/ TanStack Query Angular 版:用 mutationOptions 以类型安全的方式共享 Mutation 配置

TanStack Query Angular 版:用 mutationOptions 以类型安全的方式共享 Mutation 配置

2026-09-06 15:45:48作者:舒璇辛Bertina

在 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 mutationOptions helper. 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 的返回类型自动传播到回调参数(如 onSuccessdata)上,类型推断链路就断了。

源码实现:一个函数,三个签名

从源码看,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'
>

这组重载带来两个实际的类型语义:

  1. mutationKey 的分支:返回类型通过 WithRequiredmutationKey 固化为必填,后续消费该 options 的 API(如 queryClient.mutatesetMutationDefaults 等基于 key 的接口)可以利用 key 类型做进一步推断。
  2. 不带 mutationKey 的分支:当你只想共享 mutationFn 与回调、而 key 通过 queryClient.setMutationDefaults(['addTodo'], ...) 注册默认值时,走的是这个重载,返回类型中不含 mutationKey,与 mutations 指南setMutationDefaults 的模式配套使用。

四个泛型参数 TDataTErrorTVariablesTOnMutateResult 的默认值分别是 unknownDefaultErrorvoidunknown,与 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 的完全一致:mutationFnmutationKeyonMutateonSuccessonErroronSettledretrythrowOnError 等(完整回调签名与执行顺序参见 mutations 指南)。该函数由包入口 index.ts#L16 导出,与 queryOptionsquery-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)
      },
    })
  }
}

这里体现了两个核心收益:

  • 回调参数自动推断onSuccessnewPost 被推断为 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 即可命中默认配置。
  • 响应式 optionsinjectMutation 的入参是函数,配合 signal 可在运行时切换 mutationFnmutationKey 等字段(如上例 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)的完整类型安全链路。

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