首页
/ TanStack Query Angular 类型收窄实战:深入解析 BaseMutationNarrowing 接口

TanStack Query Angular 类型收窄实战:深入解析 BaseMutationNarrowing 接口

2026-09-07 14:53:15作者:庞眉杨Will

TanStack Query 的 Angular 版(angular-query-experimental)为 injectMutation 返回的 mutation 结果提供了一套基于类型谓词(type predicate)+ Angular Signal 的状态收窄机制。本文以 BaseMutationNarrowing 接口 为核心,梳理它的四个状态守卫属性(isSuccess/isError/isPending/isIdle)、泛型设计以及在模板与 TypeScript 中的收窄效果。读完本文,你将理解为什么官方建议用 mutation.isSuccess() 而不是 mutation.status() === 'success',并掌握如何写出既在模板中响应式、又在类型上安全可推导的 Angular mutation 代码。

BaseMutationNarrowing 是 mutation 结果类型的"收窄契约"层,源码定义于 types.ts:184,最终通过 CreateMutationResult 组合进 injectMutation 的返回值类型中。

一、为什么需要 BaseMutationNarrowing

Angular 版本的 mutation 返回值并非普通对象,而是由 signalProxy(见 signal-proxy.ts)包装的一层 Proxy:所有非函数字段被映射成 computed 信号,函数字段则原样透传(见 signal-proxy.ts:19-45)。对应的类型映射关系定义在 signal-proxy.ts:4-6

export type MapToSignals<T> = {
  [K in keyof T]: T[K] extends Function ? T[K] : Signal<T[K]>
}

也就是说,mutation.data 的类型是 Signal<TData | undefined>mutation.errorSignal<TError | null>,而 mutation.isSuccessmutation.mutate函数成员不会被包成信号。这里产生了一个关键问题:dataerrorvariables 在不同状态下可取值的区间完全不同,而普通的 Signal 类型无法表达这种随状态变化的关系。

BaseMutationNarrowing 正是为解决这个问题而设计的一层接口——它为结果对象补充了四个既是函数又是 Signal 的收窄谓词(类型别名为 SignalFunction,见 types.ts:182),当你在代码或模板里调用并"成立"时,TypeScript 可以把整个 mutation 结果收窄到对应状态子集,从而让 dataerrorvariables 的类型同步精确化。

二、泛型参数与默认值

BaseMutationNarrowing 的声明(types.ts:184-258)带四个泛型参数,全部有默认值:

泛型参数 默认值 含义
TData unknown mutation 成功后 data 的数据类型
TError DefaultError mutation 失败时 error 的类型(query-core 的 DefaultError,通常为 Error
TVariables unknown 调用 mutate 时传入的变量类型
TOnMutateResult unknown 乐观更新中 onMutate 的返回类型(供 onError/onSettledcontext 使用)

注意一个细节:虽然 CreateMutationOptionstypes.ts:126-134)中 TVariables 的默认值是 void(表示"不需要变量"),但在结果类型这条线上 TVariables 默认是 unknown。实际使用 injectMutation 时这些泛型几乎都由 mutationFn 等配置自动推断,无需手写。

在它内部还依赖两个非导出的辅助类型(位于同一文件):

  • CreateBaseMutationResulttypes.ts:154-169):用 Override 把核心的 MutationObserverResult 中的 mutate 替换为返回 voidCreateMutateFunction,并额外叠加 mutateAsync
  • CreateStatusBasedMutationResulttypes.ts:171-180):通过 Extract<CreateBaseMutationResult, { status: TStatus }> 从结果联合类型中取出某一具体 status 的分支。

三、四个状态守卫属性详解

该接口声明了四个属性,源码分别位于 types.ts 的 190、207、224、241 行。它们都被 SignalFunction<T> 包裹——即"普通谓词函数 ∩ 布尔 Signal"的交叉类型,因此既能像信号一样在模板中直接 mutation.isPending() 读取,又能作为 this is ... 谓词参与类型收窄。

isSuccess —— 成功状态守卫

源码见 types.ts:190-206

isSuccess: SignalFunction<
  (
    this: CreateMutationResult<TData, TError, TVariables, TOnMutateResult>,
  ) => this is CreateMutationResult<
    TData,
    TError,
    TVariables,
    TOnMutateResult,
    CreateStatusBasedMutationResult<
      'success',
      TData,
      TError,
      TVariables,
      TOnMutateResult
    >
  >
>

成立后,this 被收窄为 status: 'success' 的 mutation 分支:此时 data 一定存在、errornullvariables 为最后一次 mutate 传入的变量。官方类型测试(inject-mutation.test-d.ts:16-24)验证了这一点:

it('data should be defined when mutation is success', () => {
  const mutation = injectMutation(() => ({
    mutationFn: () => sleep(0).then(() => 'string'),
  }))

  if (mutation.isSuccess()) {
    expectTypeOf(mutation.data).toEqualTypeOf<Signal<string>>()
  }
})

在同一分支内 error 也会被收窄为 Signal<null>inject-mutation.test-d.ts:26-34)。

isError —— 错误状态守卫

源码见 types.ts:207-223,成立后被收窄为 status: 'error' 分支。此时 error 必然是真实的错误对象而非 nulldata 则为 undefined

it('error should be defined when mutation is error', () => {
  const mutation = injectMutation(() => ({
    mutationFn: () => sleep(0).then(() => 'string'),
  }))

  if (mutation.isError()) {
    expectTypeOf(mutation.error).toEqualTypeOf<Signal<Error>>()
  }
})

inject-mutation.test-d.ts:46-54

isPending —— 进行中守卫

源码见 types.ts:224-240,对应 status: 'pending'。mutation 在 mutate 被调用后、请求完成前处于该状态。此时 data 收窄为 undefinedinject-mutation.test-d.ts:36-44):

if (mutation.isPending()) {
  expectTypeOf(mutation.data).toEqualTypeOf<Signal<undefined>>()
}

运行时层面,Angular 实现会在此状态下通过 pendingTasks.add() 登记一个 Pending Task,并在状态离开 pending 时清理,用于确保变更检测 / 可中断渲染的稳定性(见 inject-mutation.ts:141-150)。

isIdle —— 初始待命守卫

源码见 types.ts:241-257,对应 status: 'idle'——这是 mutation 尚未执行任何操作时的初始状态,也是与 query 侧最明显的差异点:查询结果默认处于 pending,而 mutation 结果默认处于 idle。在此状态下 variablesundefined,因为还没有调用过 mutateinject-mutation.test-d.ts:56-73):

if (mutation.isIdle()) {
  expectTypeOf(mutation.variables).toEqualTypeOf<Signal<undefined>>()
}
if (mutation.isPending()) {
  expectTypeOf(mutation.variables).toEqualTypeOf<Signal<string>>()
}

四、与其他类型的组合关系

BaseMutationNarrowing 不是孤立存在的,它被 CreateMutationResult 正式"组装"为 injectMutation 的返回值类型(types.ts:260-273):

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'>>

结构拆解如下:

  • BaseMutationNarrowing 提供 4 个守卫谓词;
  • MapToSignals 把剩余的状态字段(dataerrorstatusvariablessubmittedAt 等)映射成 Signal
  • OmitKeyof<..., 'safely'> 保证两个交叉部分不发生字段冲突。

injectMutation 的实现最终把 signalProxy(resultSignal) 断言为 CreateMutationResult 返回(inject-mutation.ts:193-198)。与 query 侧对比,BaseQueryNarrowingtypes.ts:51)只有 isError/isPending/isSuccess 三个守卫、没有 isIdle,这正是查询永远不存在"空闲"语义的体现——从源码结构看,这份接口分工忠实反映了两种数据请求模型的差异。

五、模板与类型中的正确用法

模板:当作布尔信号使用

在 Angular 控制流中,四个守卫既是布尔信号又能在 @if 中做分支渲染。官方 mutations 指南(mutations.md)的完整示例:

@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)),
  }))
}

对应的运行时行为已被测试覆盖(inject-mutation.test.ts):组件初始状态四个布尔值应为 isIdle: true、其余为 false;触发 mutate 后翻转为 isPending: true;成功 / 失败时分别翻转为 isSuccess: true / isError: true(见该文件 43-46、108-111、142-145 行附近的断言)。

类型层面:收窄信号字段的类型

在 TS 逻辑中调用守卫后,分支内所有相关 Signal 字段会被同步收窄。上文测试已覆盖:isSuccess()dataSignal<string>errorSignal<null>isPending()dataSignal<undefined>isIdle()variablesSignal<undefined>;未收窄时 data 则是 Signal<string | undefined>

为什么必须用守卫而不是比较 status

从源码和官方说明(typescript.md)都可以确认一个重要结论:TypeScript 目前不支持对对象方法做可辨识联合收窄,即 mutation.status() === 'success' 不会让 mutation.data 的类型变精确;而基于信号字段 data() 直接比较也无法收窄(信号字段是 Signal<T> 对象,不是布尔值)。因此应始终优先使用 isSuccess() 这类布尔状态信号来驱动逻辑分支,例如:

// 推荐:类型可收窄
if (mutation.isSuccess()) {
  const value: string = mutation.data() // ✓ data 收窄为 string
}

// 不推荐:无法触发收窄
if (mutation.status() === 'success') {
  // mutation.data() 仍是 string | undefined
}

对 query 场景同理(query.isSuccess() 优于 query.status() === 'success'),官方在 typescript.md 的状态映射表中将 success/pending/error 等状态统一对应到各自的 isXxx() 信号,测试与测试框架(如 testing.md 中使用 mutation.isSuccess() 断言)也都遵循这一约定。

六、与 query-core 的底层映射

最后从实现上还原守卫的运行时来源:injectMutation 内部创建 @tanstack/query-coreMutationObserverinject-mutation.ts:80-83),并在订阅回调中把 state.isPending / state.isError 转写为信号结果(inject-mutation.ts:139-160)。四个守卫的 true/false 本质就是核心层 mutation 状态机(idle → pending → success | error)在 Angular 信号层上的投影:isIdle 对应初始 idleisPending 对应请求进行中,isSuccess/isError 对应两种终态。理解这层映射后,BaseMutationNarrowing 的设计动机就非常清晰——它让 Angular 开发者既能获得响应式渲染的便利,又能让 TypeScript 编译器在每一个状态分支里都给出最精确的信号类型。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388