TanStack Router ToOptions 类型详解:类型安全的导航目标描述与路由遮罩
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
本篇指南聚焦 TanStack Router(本仓库packages/*实现的客户端优先、全栈类型安全路由框架)中的ToOptions类型。ToOptions是描述"一次导航要去哪里、带什么参数、以什么方式呈现"的核心契约,useNavigate、Link、matchRoute以及redirect等 API 的参数类型都构建在它之上。读完本篇,你将掌握ToOptions各属性(from/to/hash/state/search/params/mask)的取值形态与类型收窄机制,并能基于源码理解其maskedLocation的构建逻辑。
ToOptions 是什么
官方 API 文档 ToOptionsType.md 对该类型的定义是:ToOptions包含若干用于描述一个路由目标的属性,其中包括用于路由遮罩(route masking)的mask。文档给出的类型形态如下:
type ToOptions = { from?: ValidRoutePath | string to?: ValidRoutePath | string hash?: true | string | ((prev?: string) => string) state?: true | HistoryState | ((prev: HistoryState) => HistoryState) } & SearchParamOptions & PathParamOptions & MaskOptions type SearchParamOptions = { search?: true | TToSearch | ((prev: TFromSearch) => TToSearch) } type PathParamOptions = { params?: | true | Record<string, TPathParam> | ((prev: TFromParams) => TToParams) } type MaskOptions = { mask?: ToMaskOptions<TRouter, TMaskFrom, TMaskTo> }在仓库源码中,该类型定义于 link.ts,并携带 5 个泛型参数,这正是其"全量类型安全"的来源:
// packages/router-core/src/link.ts#L360-L366 export type ToOptions< TRouter extends AnyRouter = RegisteredRouter, TFrom extends string = string, TTo extends string | undefined = '.', TMaskFrom extends string = TFrom, TMaskTo extends string = '.', > = ToSubOptions<TRouter, TFrom, TTo> & MaskOptions<TRouter, TMaskFrom, TMaskTo>TRouter:具体路由实例类型,决定路由树、各路由的allParams与fullSearchSchema;TFrom:导航出发路由路径字面量;TTo:目标路径字面量('.'表示当前路由);TMaskFrom/TMaskTo:遮罩路由的出发/目标路径,独立参与类型收窄。
文档中的ValidRoutePath、TToSearch等是简化的泛型占位符,源码中分别由ToPathOption/FromPathOption等约束类型展开,下文逐属性展开。
属性总览
| 属性 | 类型 | 说明 |
|---|---|---|
from | RoutePaths<TRouter['routeTree']> | 出发路由;useNavigate场景下通常由getRouteApi自动提供,此处主要用于链接/跨路由导航 |
to | ToPathOption<TRouter, TFrom, TTo> | 目标路由路径,支持绝对路径与相对路径(如'../profile'),并带编辑器路径补全 |
hash | true \| Updater<string> | 锚点;true表示保留当前 hash |
state | true \| NonNullableUpdater<ParsedHistoryState, HistoryState> | history state;true表示保留当前 state |
search | true/ 对象 /(prev) => 对象 | 目标搜索参数,按目标路由的validateSearch校验 |
params | true/Record<string, TPathParam>/(prev) => 对象 | 路径参数,按目标路由的allParams校验 |
mask | ToMaskOptions<TRouter, TMaskFrom, TMaskTo> | 路由遮罩:实际路由与展示路径分离 |
unsafeRelative | 'path' | 源码级逃生舱,仅 link.ts 中定义,文档未列出 |
from 与 to:路径约束与自动补全
to与from的类型并非简单的string。源码 link.ts#L616-L632 给出:
export type ToPathOption< TRouter extends AnyRouter = AnyRouter, TFrom extends string = string, TTo extends string | undefined = string, > = ConstrainLiteral< TTo, RelativeToPathAutoComplete< TRouter, NoInfer<TFrom> extends string ? NoInfer<TFrom> : '', NoInfer<TTo> & string > > export type FromPathOption<TRouter extends AnyRouter, TFrom> = ConstrainLiteral< TFrom, RoutePaths<TRouter['routeTree']> >从源码结构看,ToPathOption会在三种候选集之间做联合:绝对路径(RouteToPath,即整棵路由树的路径)、相对当前路由可达的路径(RelativeToCurrentPath)、相对父级可达的路径(RelativeToParentPath,处理'../x'形式)。ConstrainLiteral的作用是把泛型TTo约束回这些候选字面量上——写错路径会在编译期直接报错,编辑器中还能获得路径补全。
to是必选还是可选由MakeToRequired决定(link.ts#L421-L431):当TFrom无法推断(退化为string)时放宽为可选,避免在未绑定具体路由上下文的场景误报;能确定TFrom且TTo是具体字面量时则要求必填。
hash 与 state:三种取值形态
hash和state采用统一的"三态"设计,源码定义见 link.ts#L433-L442:
export type ToSubOptionsProps< TRouter extends AnyRouter = RegisteredRouter, TFrom extends RoutePaths<TRouter['routeTree']> | string = string, TTo extends string | undefined = '.', > = MakeToRequired<TRouter, TFrom, TTo> & { hash?: true | Updater<string> state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState> from?: FromPathOption<TRouter, TFrom> & {} unsafeRelative?: 'path' }其中Updater/NonNullableUpdater定义在 utils.ts#L87-L91,本质是TResult | ((prev: TPrevious) => TResult)联合。因此:
hash: true:导航后保留地址栏现有锚点;hash: '#section':直接覆盖;hash: (prev) => next:基于旧锚点函数式更新;state: true:保留当前 history state;传对象则整体替换;传函数时,入参prev的类型是ParsedHistoryState(比HistoryState多携带路由内部元数据),返回值是普通HistoryState。这与文档中(prev: HistoryState) => HistoryState的简化写法语义一致,但源码的prev更精确。
配合NavigateOptionProps(见replace、resetScroll、viewTransition等,link.ts#L301-L358),ToOptions构成完整的导航描述。
search 与 params:按需必填的类型收窄
search和params的可选性不是固定的,而是由"目标路由是否要求这些参数"动态推导的。源码链路如下(link.ts#L541-L614):
export interface MakeOptionalSearchParams<...> { search?: true | (ParamsReducer<TRouter, 'SEARCH', TFrom, TTo> & {}) } export interface MakeRequiredSearchParams<...> { search: MakeRequiredParamsReducer<TRouter, 'SEARCH', TFrom, TTo> & {} } export type SearchParamOptions<TRouter, TFrom, TTo> = IsRequired<TRouter, 'SEARCH', TFrom, TTo> extends never ? MakeOptionalSearchParams<TRouter, TFrom, TTo> : MakeRequiredSearchParams<TRouter, TFrom, TTo>IsRequired(link.ts#L590-L604)会把TFrom、TTo经ResolveRelativePath解析成真实路径,再取目标路由的allParams/fullSearchSchemaInput做必填性判断。由此得到两种编译期行为:
- 目标路由有必填 search/params:
search/params变成必填属性,缺省会直接报错(MakeRequiredParamsReducer还允许传true继承当前值,前提是当前参数已满足目标要求); - 目标路由参数全部可选:两者退化为可选。
参数值本身支持三种形态,ParamsReducer定义见 link.ts#L444-L460:
true:保留出发路由上同名参数的当前值;- 字面量对象:全量替换,
params: { id: '42' }; - 更新函数:
search: (prev) => ({ ...prev, tab: 'settings' }),函数入参是出发路由的完整参数/搜索类型,返回值按目标路由的输入 schema 校验。
一个可复制的典型用法(与上述类型完全吻合):
// 从 /user 导航到 /user/$id,保留当前 search,仅更新其中 tab router.navigate({ to: '/user/$id', params: { id: '42' }, search: (prev) => ({ ...prev, tab: 'settings' }), })MaskOptions:路由遮罩(route masking)
mask是ToOptions中最有特色的部分。遮罩的含义是:实际渲染某条路由,但地址栏展示另一条路径——典型场景是把详情页以模态框形式呈现,分享链接时再"解除遮罩"回到真实 URL。
源码中MaskOptions与ToMaskOptions定义在 link.ts#L368-L383:
export interface MaskOptions< in out TRouter extends AnyRouter, in out TMaskFrom extends string, in out TMaskTo extends string, > { _fromLocation?: ParsedLocation mask?: ToMaskOptions<TRouter, TMaskFrom, TMaskTo> } export type ToMaskOptions< TRouter extends AnyRouter = RegisteredRouter, TMaskFrom extends string = string, TMaskTo extends string = '.', > = ToSubOptions<TRouter, TMaskFrom, TMaskTo> & { unmaskOnReload?: boolean }注意ToMaskOptions本身递归复用ToSubOptions——也就是说mask内部可以带自己的to/params/search,且这些参数按遮罩路由(TMaskTo)的类型收窄,而非真实目标路由,这是TMaskFrom/TMaskTo两个独立泛型的意义。unmaskOnReload控制页面刷新后是否解除遮罩。
源码如何消费 mask
RouterCore构建 location 时(router.ts#L2146-L2175):
const next = build(opts) if (opts.mask) { next.maskedLocation = build({ from: opts.from, ...opts.mask, }) } else if (this.options.routeMasks) { const match = findFlatMatch<RouteMask<TRouteTree>>( next.pathname, this.processedTree, ) if (match) { const params = Object.assign(Object.create(null), match.rawParams) const { from: _from, params: maskParams, ...maskProps } = match.route const nextParams = resolveNextParams(maskParams, params) next.maskedLocation = build({ from: opts.from, ...maskProps, params: nextParams, }) } }从源码结构看,遮罩解析有两条路径:
- 显式
opts.mask:以同一套build逻辑基于mask.to额外构建一个maskedLocation,与真实 location 并存; - 路由树级
routeMasks:未显式传mask时,若构建后的真实路径命中了路由树中的某个RouteMask(由createRouteMask声明),则自动套用该 mask 的to/ 默认params。params也支持函数形式,会以真实路由匹配到的参数为上下文求值。
提交到 history 时,unmaskOnReload的取值优先级为:导航级选项 > 遮罩自身选项 > 路由级全局unmaskOnReload(见 router.ts#L2249-L2253 的??链)。后续读取 location 时,对外暴露的一律是location.maskedLocation ?? location(router.ts#L2354、#L2530),保证useLocation等 hook 拿到的始终是"用户可见"的地址。
完整的路由遮罩概念、模态框案例与刷新解除行为,可继续阅读 route-masking 指南 与 RouteMaskType.md、ToMaskOptionsType.md;仓库内 examples/react/location-masking 提供了一个可运行的遮罩示例应用。
ToOptions 在上层 API 中的复用
ToOptions是整个导航体系的公共底座,NavigateOptions直接在其上叠加行为开关(link.ts#L289-L295):
export type NavigateOptions< TRouter extends AnyRouter = RegisteredRouter, TFrom extends string = string, TTo extends string | undefined = '.', TMaskFrom extends string = TFrom, TMaskTo extends string = '.', > = ToOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & NavigateOptionProps由此形成的复用关系(均以源码为据):
useNavigate返回的navigate接受NavigateOptions(useNavigate.ts);Link的to相关 props 是NavigateOptions & LinkOptionsProps(link.ts#L702-L704),因此<Link to mask search params>与编程式导航享受同一套类型收窄;redirect()/createRedirect()同样以NavigateOptions为参数(redirect.ts#L13-L16),loader 内跳转时可用完全一致的写法;matchRoute的路由位置参数直接就是ToOptions(router.ts#L723-L744),用于在组件外判断"某位置是否命中某路由并取出参数";preloadRoute亦以NavigateOptions为参数(router.ts#L696-L721)。
行为类开关(replace、resetScroll、viewTransition、ignoreBlocker、reloadDocument、href、hashScrollIntoView)不属于ToOptions本体,而属于NavigateOptionProps;ToOptions只负责"去哪里、带什么、以什么面貌呈现"。更多说明见 NavigateOptionsType.md 与 LinkOptionsType.md。
小结
ToOptions是 TanStack Router 中导航目标的类型契约:from/to决定去向且带路径补全,hash/state支持true、字面量、更新函数三态,search/params按目标路由 schema 动态决定必填性与校验,mask提供真实路由与展示路径分离的遮罩能力;- 其可选性/必填性由
IsRequired+MakeRequired/Optional*条件类型在编译期推导(link.ts#L590-L614),把运行时参数错误前移到类型检查阶段; mask的最终落地是maskedLocation的并行构建(router.ts#L2148-L2172),并可通过routeMasks在路由树上声明式复用;- 需要继续深入时,建议按 ToOptionsType.md → NavigateOptionsType.md → route-masking 指南 → examples/react/location-masking 的顺序阅读。
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考