首页
/ TanStack Query Angular 的 DefinedInitialDataOptions 类型解析:从"有初始数据"的类型契约到 injectQuery 结果收窄

TanStack Query Angular 的 DefinedInitialDataOptions 类型解析:从"有初始数据"的类型契约到 injectQuery 结果收窄

2026-09-07 13:49:10作者:伍霜盼Ellen

导读

DefinedInitialDataOptions 是 TanStack Query Angular 适配层(angular-query-experimental)中描述"在声明时已经携带确定初始数据"的查询配置选项的类型别名。理解它,就能理解为什么 injectQuery 在提供 initialData 后会返回 data 永不包含 undefinedDefinedCreateQueryResult,从而在模板与 TS 严格模式中获得免判空的安全访问。本文以该类型别名的完整声明为骨架,结合其定义源码、injectQuery 重载机制以及 query-core 底层状态初始化逻辑,逐层拆解这一类型契约的含义与实战用法。

一个类型,先回答"查询是否有初始数据"

query-options.ts 中,Angular 适配层把查询选项在类型层面分成了三种形态:

类型别名 initialData 语义
UndefinedInitialDataOptions 可选(可缺省、可为 undefined 查询可能没有任何缓存数据,status 可能为 pending
UnusedSkipTokenOptions 不含(queryFn 排除 SkipToken 用于确定 queryFn 必然被执行的场景
DefinedInitialDataOptions 必填,且取值不允许为 undefined 查询在创建时就保证有数据可展示

DefinedInitialDataOptions 正是第三种:"定义即确定"。它通过把 initialData 设为必填属性,在编译期向 TypeScript 承诺——该查询从诞生那一刻起就拥有可渲染的数据,后续的 injectQuery 返回值也会相应收窄。

完整类型声明与逐项解读

别名骨架

type DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> = Omit<
  CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  'queryFn'
> & object;

含义拆解:

  1. CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey> 是 Angular 侧的"标准查询选项"基底。它定义在 types.ts,本质是 CreateBaseQueryOptions 去除 suspense 键的结果,而 CreateBaseQueryOptions 又继承自 query-core 的 QueryObserverOptions——也就是说 queryKeyqueryFnstaleTimegcTimeenabledrefetchInterval 等全部查询选项都来自这里。
  2. Omit<..., 'queryFn'> 先移除 queryFn(因为"依赖纯初始数据即可运行"的场景未必需要真正的请求函数)。
  3. & object 后的块重新声明了两个受约束的属性(见下)。

initialData(必填)

initialData:
  | NonUndefinedGuard<TQueryFnData>
  | (() => NonUndefinedGuard<TQueryFnData>);

两个要点:

  • 它是必填的(无 ?。对比 UndefinedInitialDataOptionsinitialData? 的可选声明,这正是二者在类型层面的分水岭。
  • 取值永不可能是 undefinedNonUndefinedGuard<T> 定义在 query-core/src/types.ts
export type NonUndefinedGuard<T> = T extends undefined ? never : T

即:若泛型 TQueryFnData 恰好推断为 undefined,则整个类型被折叠为 never,赋值直接报错。它既接受一个确定值 TQueryFnData,也接受一个返回该值的惰性函数 () => TQueryFnData

queryFn?(可选)

optional queryFn: QueryFunction<TQueryFnData, TQueryKey>;

UndefinedInitialDataOptions 等一致,queryFn 被保留为可选:当你只想把预置数据当作"瞬时缓存种子"、等后台异步刷新时才需要请求函数;若数据已完整且永远无需联网,queryFn 甚至可以省略。

四个类型参数及默认值

类型别名携带四个泛型参数,含义与 query-core 保持完全一致:

类型参数 默认值 约束 说明
TQueryFnData unknown queryFn 原始返回值类型
TError DefaultError 错误类型,默认由 query-core 的 Register 接口 决定(通常为 Error
TData TQueryFnData 经过 select 变换后真正暴露给 UI 的数据类型
TQueryKey QueryKey extends QueryKey 查询键类型,默认为只读数组 ReadonlyArray<unknown>,见 types.ts

关于 DefaultError,其实现通过 Register extends { defaultError: infer TError } 的模块增强(module augmentation)机制取值:开发者可扩展 Register 接口全局替换默认错误类型,未扩展时回退到 Error

为什么叫 "Defined":injectQuery 重载与返回类型收窄

DefinedInitialDataOptions 的真正价值体现在 inject-query.ts 的同名重载里:

export function injectQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  injectQueryFn: () => DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
  options?: InjectQueryOptions,
): DefinedCreateQueryResult<TData, TError>

而另一个重载(inject-query.ts)接受 UndefinedInitialDataOptions,返回的是普通 CreateQueryResult<TData, TError>

因此,只要你传入的 options 函数返回带必填 initialData 的配置,TypeScript 便自动命中第一个重载,得到 DefinedCreateQueryResult。定义见 types.ts

export type DefinedCreateQueryResult<
  TData = unknown,
  TError = DefaultError,
  TState = DefinedQueryObserverResult<TData, TError>,
> = BaseQueryNarrowing<TData, TError> &
  MapToSignals<OmitKeyof<TState, keyof BaseQueryNarrowing, 'safely'>>

底层的 DefinedQueryObserverResult(见 query-core/src/types.ts)保证其 data 属性不为 undefined,并经由 MapToSignals 映射为 Angular 的 signal 形态。结果是模板或组件里可以放心书写 query.data(),不再需要 if (query.data() === undefined) 之类的守卫。

类型推断实战对照

// 不提供 initialData —— 走 UndefinedInitialDataOptions 重载
query1 = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetchTodos(),
}))
// query1.data(): Todo[] | undefined,需要判空

// 提供必填 initialData —— 命中 Defined 重载
query2 = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetchTodos(),
  initialData: [],
}))
// query2.data(): Todo[],可直接访问,无需判空

这正是类型别名名称 "Defined"(已确定)的含义:它不仅描述"配置里有一个初值",更把这种确定性一路传递到查询结果类型。

实战用法:initialData 的四种打开方式

Angular 官方指南 initial-query-data.md 给出了完整的配套用法,下面全部是 injectQuery 内的直接示例。

1. 直接提供静态初值

result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: initialTodos,
}))

效果:组件/服务实例一创建就立即显示 initialTodos,同时后台立刻发起一次 refetch 用服务器数据替换它。

2. 配合 staleTime 延迟刷新

result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: initialTodos,
  staleTime: 1000, // 1 秒内数据视为新鲜,不触发后台刷新
}))

staleTime 窗口内,查询认为自身数据仍是新鲜的,fetchStatus 不会进入 fetching,避免"一进来就闪一次加载"的抖动。指南中还给出 staleTime: 60 * 1000 配合 initialDataUpdatedAt 的更精细版本。

3. 使用函数形式延迟计算

result = injectQuery(() => ({
  queryKey: ['todos'],
  queryFn: () => fetch('/todos'),
  initialData: () => getExpensiveTodos(), // 惰性计算,仅在该查询真正初始化时才求值
}))

函数形式对应类型声明中 (() => NonUndefinedGuard<TQueryFnData>) 的分支,适合初始化成本较高、希望推迟到查询创建时才执行的场景(例如从内存缓存或本地存储读取)。

4. 跨查询派生:把列表数据当作详情页初值

这是"优化第二屏体验"的经典做法——把某个已就绪查询的数据当作另一个查询的 initialData

result = injectQuery(() => ({
  queryKey: ['todo', this.todoId()],
  queryFn: () => fetch(`/todos/${this.todoId()}`),
  initialData: () =>
    queryClient.getQueryData(['todos'])?.find((d) => d.id === this.todoId()),
}))

配合 initialDataUpdatedAt 让时间戳也一并透传:

result = injectQuery(() => ({
  queryKey: ['todo', this.todoId()],
  queryFn: () => fetch(`/todos/${this.todoId()}`),
  initialData: () =>
    queryClient.getQueryData(['todos'])?.find((d) => d.id === this.todoId()),
  initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt,
}))

若想只接受"10 秒内"的旧数据,过期则放弃初值、退回硬加载态,可以在函数内部访问 getQueryState 并返回 undefined——但注意此时类型上会与 DefinedInitialDataOptionsnever 约束冲突,实际项目中这类"可能返回 undefined 的初值函数"应选择 UndefinedInitialDataOptions 一方(即不声明 initialData,或走允许 undefined 的重载),这也反向印证了两类类型别名分工的必要性。

底层原理:getDefaultState 如何消费 initialData

类型声明承诺"有初值",运行时则由 query-core 的查询状态工厂兑现。在 query.tsgetDefaultState 中可以看到完整逻辑:

const data =
  typeof options.initialData === 'function'
    ? (options.initialData as InitialDataFunction<TData>)()
    : options.initialData

const hasData = data !== undefined

const initialDataUpdatedAt = hasData
  ? typeof options.initialDataUpdatedAt === 'function'
    ? options.initialDataUpdatedAt()
    : options.initialDataUpdatedAt
  : 0

return {
  data,
  ...
  dataUpdatedAt: hasData ? (initialDataUpdatedAt ?? Date.now()) : 0,
  ...
}

三个关键点:

  1. 统一函数分支:无论类型声明里 initialData 是值还是函数,运行时都先经 typeof === 'function' 判断后求值——与类型联合 TQueryFnData | (() => TQueryFnData) 一一对应(InitialDataFunction<T> 定义见 types.ts)。
  2. 有数据即 success 起点:只要求值结果 !== undefined,默认状态就携带真实 data,因此观察者初始便处于"有数据"状态,UI 首次渲染不会落入 loading。
  3. initialDataUpdatedAt 决定陈旧性:该字段可为时间戳数字或返回时间戳的函数(types.ts);未提供时回退为 Date.now()。由于默认状态把 dataUpdatedAt 记为当前时间,若不显式提供旧时间戳,staleTime 的判断会认为初值"刚刚新鲜"。query-core 的测试用例也验证了这一点,例如 query.test.tsx 中"函数形式 initialDataUpdatedAt 会被调用"以及"initialDataUpdatedAt 为 0 时也正常工作"。

initialData 与 placeholderData:别混淆两种"提前展示"

类型上 DefinedInitialDataOptions 只约束 initialData,但与之经常被对比的 placeholderData(见 types.ts 注释:placeholder 用于查询仍处于 loading 且未提供 initialData 时)也在 CreateQueryOptions 基座中可用。两者在语义与运行时行为上有本质区别:

维度 initialData(Defined) placeholderData
是否"真正的数据" 是,会写入查询缓存,作为状态初始值 否,仅临时展示层占位,不进入缓存
状态推导 有值时初始 statussuccess 查询本身仍可能 pending
结果类型 收窄为 DefinedCreateQueryResultdata 非空 不影响结果类型收窄
典型用途 从缓存/持久层"种子化"一个真实查询 分页等场景保留上一页内容(如 keepPreviousData

占位数据的完整用法可参考 placeholder-query-data.md,其中包含 placeholderData: (previousData, previousQuery) => previousData 这样的"上一页数据续显"模式。

配套设施:queryOptions 复用同一份类型

DefinedInitialDataOptions 还出现在 queryOptions() 的重载签名中——该工具函数用于把某份查询配置抽离为可共享、可复用且类型安全的选项对象,其第一组重载恰好接收 DefinedInitialDataOptions。换言之,把带 initialData 的查询配置放进 queryOptions()(或用 prefetchQuery/setQueryData 预填充缓存)后,injectQuery(() => sharedOptions) 依然能获得定义即确定的类型体验,这一模式非常适合多个组件共享同一数据源的场景。

小结

DefinedInitialDataOptions 的价值不在"多了一个必填字段",而在于它把"我有初始数据"这一业务事实编码进了 TypeScript 类型系统:injectQuery 依据它命中重载、返回 data 非空的 DefinedCreateQueryResult,query-core 的 getDefaultState 又依据它让查询从 success 而非 pending 起步。前后端(类型契约与运行时)互为印证,这正是 TanStack Query 以类型驱动 API 设计的缩影。当你在 Angular 应用中希望"首帧即有数据、无 undefined 噪音"时,它就是你需要的那一层类型契约。

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