#@guanriyue/decurl/routeSpec
@guanriyue/decurl/routeSpec 用于集中定义 React Router path 与 Search Fields,并从业务值生成类型安全的应用内 href。
import { routeSpec } from '@guanriyue/decurl/routeSpec';RouteSpec 是 URL contract 和 href builder,不定义 component、loader、action 或路由层级。
#routeSpec
#调用形式
routeSpec({
path,
search?,
})下面的定义会保留 path pattern 和 Search Fields 的具体类型:
import { defineFields, field } from '@guanriyue/decurl/codec';
import { routeSpec } from '@guanriyue/decurl/routeSpec';
const usersSearch = defineFields({
keyword: field({
decode: (input) => input || undefined,
}),
page: field({
decode: (input) => {
const value = Number(input);
return Number.isSafeInteger(value) && value >= 1
? value
: undefined;
},
defaultValue: 1,
}),
});
const to = {
users: routeSpec({
path: '/users',
search: usersSearch,
}),
userDetail: routeSpec({
path: '/users/:id',
}),
};#Options
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
path | `/${string}` | 是 | - | React Router path pattern。 |
search | RecordCodec | 否 | undefined | 用于生成 search params 的 Search Fields。 |
path 在语义上只包含 pathname pattern,不应包含 search 或 hash。
#返回值
routeSpec 返回一个可调用对象:
| 成员 | 说明 |
|---|---|
spec(input?) | 使用扁平输入生成 href。 |
spec.path | 原始 React Router path pattern,保留字面量类型。 |
spec.search | 原始 Search Fields;未定义时为 undefined。 |
spec.hrefByParts(input) | 分别传入 path params 与 search input,并生成 href。 |
生成结果是应用内部 href string:
to.userDetail({ id: 42 });
// /users/42
to.users({ keyword: 'type safe', page: 2 });
// /users?keyword=type+safe&page=2如果最终没有生成 search params,返回值不会追加 ?。
#输入是否必填
RouteSpec 会根据 path params 和 search input 决定 callable input 是否必填:
const home = routeSpec({ path: '/' });
const categories = routeSpec({ path: '/:lang?/categories' });
home();
categories();
categories({ lang: 'zh' });
to.users();
to.userDetail({ id: 42 });- 没有 path params 和 search 时,不接收 input。
- 只有可选 path params 或 search 时,input 可以省略。
- 存在必需 path param 时,input 必填。
#Path params
Path params 的推导和 pathname 生成遵循 React Router path pattern:
| 形式 | 示例 | 输入 |
|---|---|---|
| 必需参数 | /users/:id | id: string | number | boolean |
| 可选参数 | /:lang?/categories | lang?: string | number | boolean | null | undefined |
| Splat | /files/* | '*': string | number | boolean |
非 nullish path param 会先通过 String(value) 转换,再交给 React Router generatePath。pathname params 的编码行为遵循 React Router,RouteSpec 不重新实现 path encode:
const files = routeSpec({ path: '/files/*' });
files({ '*': 'docs/getting-started.md' });
// /files/docs/getting-started.md#Search
search 直接复用 Search Fields。生成 href 时,RouteSpec 只负责把 search input 交给相同的 codec encode 能力,不重新定义 search 编码规则。
to.users({ keyword: '@guanriyue/decurl', page: 2 });Search input 是 partial object;具体字段接受什么业务值,以及如何写入 URL,由对应 FieldCodec 决定。
RouteSpec 不负责 search decode。读取 URL 时,页面可以直接复用它保存的 Search Fields:
const UsersPage = () => {
const [search] = useSearchValues(to.users.search);
return <UsersTable search={search} />;
};#Flat input
直接调用 RouteSpec 时,path params 和 search properties 合并为一个扁平对象:
const userDetail = routeSpec({
path: '/users/:id',
search: defineFields({
tab: field({
decode: (input) => input || undefined,
}),
}),
});
userDetail({
id: 42,
tab: 'profile',
});冲突判断基于 path param 与 Search Fields 的 property key。如果两部分存在同名 property,flat input 会在类型层被禁用,此时必须使用 hrefByParts。
import { const defineFields: <TDefinition extends RecordCodec>(definition: TDefinition) => DefinedFields<TDefinition> defineFields , function field<TValue>(definition: SingleOptionalFieldCodec<TValue> & {
name: DefinedFieldName;
}): WithDefinedFieldName<SingleOptionalFieldCodec<TValue>> (+7 overloads)
field } from '@guanriyue/decurl/codec';
import { const routeSpec: <const TPath extends RouteSpecPath, const TSearch extends RecordCodec | undefined = undefined>(options: RouteSpecOptions<TPath, TSearch>) => RouteSpec<TPath, RouteSpecPathParams<TPath>, TSearch>定义一个可调用的路由规格。
调用返回由 path params 和 search params 共同生成的 href,同时保留原始
path pattern、search definition 和分组生成 API。
@exampleconst userDetail = routeSpec({
path: '/users/:id',
search: userSearch,
})
userDetail({ id: 1, tab: 'profile' })
userDetail.path
routeSpec } from '@guanriyue/decurl/routeSpec';
const const detail: RouteSpec<"/users/:id", {
id: string | number | boolean;
}, {
id: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
}, never>
detail = routeSpec<"/users/:id", {
id: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
}>(options: RouteSpecOptions<"/users/:id", {
id: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
}>): RouteSpec<"/users/:id", {
id: string | number | boolean;
}, {
id: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
}, never>
定义一个可调用的路由规格。
调用返回由 path params 和 search params 共同生成的 href,同时保留原始
path pattern、search definition 和分组生成 API。
@exampleconst userDetail = routeSpec({
path: '/users/:id',
search: userSearch,
})
userDetail({ id: 1, tab: 'profile' })
userDetail.path
routeSpec ({
path: "/users/:id"React Router path pattern。
path : '/users/:id',
search?: {
id: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
} | undefined
用于编码和解码 search params 的字段定义。
search : defineFields<{
id: SingleOptionalFieldCodec<string>;
}>(definition: {
id: SingleOptionalFieldCodec<string>;
}): {
id: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
}
defineFields ({
id: SingleOptionalFieldCodec<string> id : field<string>(definition: SingleOptionalFieldCodec<string>): SingleOptionalFieldCodec<string> (+7 overloads) field ({
decode: Decode<string, string> decode : (input: string input ) => input: string input || var undefined undefined ,
}),
}),
});
const detail: RouteSpec
(input: never) => string
detail ({ id: 42 });Argument of type '{ id: number; }' is not assignable to parameter of type 'never'.#hrefByParts
hrefByParts 分别接收 path params 和 search input,是无歧义的分组生成 API:
const detail = routeSpec({
path: '/users/:id',
search: defineFields({
id: field({
decode: (input) => input || undefined,
}),
}),
});
detail.hrefByParts({
params: { id: 42 },
search: { id: 'filter-id' },
});
// /users/42?id=filter-id| 部分 | 必填规则 | 说明 |
|---|---|---|
params | 存在必需 path param 时必填 | 只用于生成 pathname。 |
search | 否 | 只用于生成 search params。 |
省略可选部分时,运行时按空对象处理。hrefByParts 的 input 对象本身始终需要传入。
#配合 React Router
#Link 与 navigate
RouteSpec 返回 string,可以直接交给 React Router:
<Link to={to.userDetail({ id: userId })}>
Detail
</Link>const navigate = useNavigate();
navigate(to.users({ keyword: '@guanriyue/decurl' }));#Router config
Router config 可以复用 RouteSpec 保存的 path pattern:
const routes = [
{
path: to.users.path,
element: <UsersPage />,
},
{
path: to.userDetail.path,
element: <UserDetailPage />,
},
];RouteSpec 不保存 parent/child 关系;对象或目录层级也不会产生额外路由语义。
#Types
公开类型提取器分别对应 path params、search encode input 和 search decode values:
import { const defineFields: <TDefinition extends RecordCodec>(definition: TDefinition) => DefinedFields<TDefinition> defineFields , function field<TValue>(definition: SingleOptionalFieldCodec<TValue> & {
name: DefinedFieldName;
}): WithDefinedFieldName<SingleOptionalFieldCodec<TValue>> (+7 overloads)
field } from '@guanriyue/decurl/codec';
import {
type type InferRouteSpecParams<TSpec> = RouteSpecAssociatedTypesOf<TSpec> extends {
params: infer TParams;
} ? TParams : never
提取 RouteSpec 生成 pathname 时接受的 path params 类型。
该类型会保留 RouteSpec 对 path params 的显式类型约束,适合用于封装导航函数、
预加载函数或其他需要单独接收 path params 的模块边界。
@exampleconst userDetail = routeSpec({
path: '/users/:id',
})
type UserDetailParams = InferRouteSpecParams<typeof userDetail>
function preloadUser(params: UserDetailParams) {
return queryClient.prefetchQuery({
queryKey: ['user', params.id],
})
}
InferRouteSpecParams ,
type type InferRouteSpecSearchInput<TSpec> = RouteSpecAssociatedTypesOf<TSpec> extends {
searchInput: infer TInput;
} ? TInput : never
提取 RouteSpec 生成 href 时接受的 search 输入类型。
这是从业务值写入 URL 的输入类型。字段可以按需传入,并允许使用 null 或
undefined 表示不写入或移除对应的 search param。它适合用于搜索表单、导航
函数和 hrefByParts 的 search 参数。
它不同于
InferRouteSpecSearchValues
:后者表示从 URL 解码后得到的完整
页面状态,而不是生成 URL 时接受的 partial 输入。
@exampleconst orders = routeSpec({
path: '/orders',
search: ordersSearch,
})
type OrdersSearchInput = InferRouteSpecSearchInput<typeof orders>
function openOrders(search: OrdersSearchInput) {
navigate(orders(search))
}
openOrders({
keyword: 'decurl',
page: 2,
})
InferRouteSpecSearchInput ,
type type InferRouteSpecSearchValues<TSpec> = RouteSpecAssociatedTypesOf<TSpec> extends {
searchValues: infer TValues;
} ? TValues : never
提取 RouteSpec 从 URL 解码 search params 后得到的业务值类型。
这是从 URL 读取数据的结果类型。它会反映 codec 的 optional 和 defaultValue
语义:可选字段可能是 undefined,具有默认值的字段则是必需值。它适合用于
页面状态、组件 props,以及消费 useSearchValues 解码结果的业务函数。
它不同于
InferRouteSpecSearchInput
:后者是生成 URL 时接受的 partial
输入,因此字段可选并可能接受 nullish 值。
@exampleconst orders = routeSpec({
path: '/orders',
search: ordersSearch,
})
type OrdersSearchValues = InferRouteSpecSearchValues<typeof orders>
type OrdersTableProps = {
search: OrdersSearchValues
}
function OrdersPage() {
const [search] = useSearchValues(orders.search)
return <OrdersTable search={search} />
}
InferRouteSpecSearchValues ,
const routeSpec: <const TPath extends RouteSpecPath, const TSearch extends RecordCodec | undefined = undefined>(options: RouteSpecOptions<TPath, TSearch>) => RouteSpec<TPath, RouteSpecPathParams<TPath>, TSearch>定义一个可调用的路由规格。
调用返回由 path params 和 search params 共同生成的 href,同时保留原始
path pattern、search definition 和分组生成 API。
@exampleconst userDetail = routeSpec({
path: '/users/:id',
search: userSearch,
})
userDetail({ id: 1, tab: 'profile' })
userDetail.path
routeSpec ,
} from '@guanriyue/decurl/routeSpec';
const const searchFields: {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}
searchFields = defineFields<{
keyword: SingleOptionalFieldCodec<string>;
page: SingleRequiredFieldCodec<number>;
}>(definition: {
keyword: SingleOptionalFieldCodec<string>;
page: SingleRequiredFieldCodec<number>;
}): {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}
defineFields ({
keyword: SingleOptionalFieldCodec<string> keyword : field<string>(definition: SingleOptionalFieldCodec<string>): SingleOptionalFieldCodec<string> (+7 overloads) field ({
decode: Decode<string, string> decode : (input: string input ) => input: string input || var undefined undefined ,
}),
page: SingleRequiredFieldCodec<number> page : field<number>(definition: SingleRequiredFieldCodec<number>): SingleRequiredFieldCodec<number> (+7 overloads) field ({
decode: Decode<string, number> decode : (input: string input ) => {
const const value: number value = var Number: NumberConstructor
(value?: any) => number
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Number (input: string input );
return var Number: NumberConstructorAn object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Number .NumberConstructor.isSafeInteger(number: unknown): booleanReturns true if the value passed is a safe integer.
@paramnumber A numeric value. isSafeInteger (const value: number value ) && const value: number value >= 1
? const value: number value
: var undefined undefined ;
},
defaultValue: number defaultValue : 1,
}),
});
const const users: RouteSpec<"/users/:groupId", {
groupId: string | number | boolean;
}, {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}, never>
users = routeSpec<"/users/:groupId", {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}>(options: RouteSpecOptions<"/users/:groupId", {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}>): RouteSpec<...>
定义一个可调用的路由规格。
调用返回由 path params 和 search params 共同生成的 href,同时保留原始
path pattern、search definition 和分组生成 API。
@exampleconst userDetail = routeSpec({
path: '/users/:id',
search: userSearch,
})
userDetail({ id: 1, tab: 'profile' })
userDetail.path
routeSpec ({
path: "/users/:groupId"React Router path pattern。
path : '/users/:groupId',
search?: {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
} | undefined
用于编码和解码 search params 的字段定义。
search : const searchFields: {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}
searchFields ,
});
type type UsersParams = {
groupId: string | number | boolean;
}
UsersParams = type InferRouteSpecParams<TSpec> = RouteSpecAssociatedTypesOf<TSpec> extends {
params: infer TParams;
} ? TParams : never
提取 RouteSpec 生成 pathname 时接受的 path params 类型。
该类型会保留 RouteSpec 对 path params 的显式类型约束,适合用于封装导航函数、
预加载函数或其他需要单独接收 path params 的模块边界。
@exampleconst userDetail = routeSpec({
path: '/users/:id',
})
type UserDetailParams = InferRouteSpecParams<typeof userDetail>
function preloadUser(params: UserDetailParams) {
return queryClient.prefetchQuery({
queryKey: ['user', params.id],
})
}
InferRouteSpecParams <typeof const users: RouteSpec<"/users/:groupId", {
groupId: string | number | boolean;
}, {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}, never>
users >;
type type UsersSearchInput = {
keyword?: string | null | undefined;
page?: number | null | undefined;
}
UsersSearchInput = type InferRouteSpecSearchInput<TSpec> = RouteSpecAssociatedTypesOf<TSpec> extends {
searchInput: infer TInput;
} ? TInput : never
提取 RouteSpec 生成 href 时接受的 search 输入类型。
这是从业务值写入 URL 的输入类型。字段可以按需传入,并允许使用 null 或
undefined 表示不写入或移除对应的 search param。它适合用于搜索表单、导航
函数和 hrefByParts 的 search 参数。
它不同于
InferRouteSpecSearchValues
:后者表示从 URL 解码后得到的完整
页面状态,而不是生成 URL 时接受的 partial 输入。
@exampleconst orders = routeSpec({
path: '/orders',
search: ordersSearch,
})
type OrdersSearchInput = InferRouteSpecSearchInput<typeof orders>
function openOrders(search: OrdersSearchInput) {
navigate(orders(search))
}
openOrders({
keyword: 'decurl',
page: 2,
})
InferRouteSpecSearchInput <typeof const users: RouteSpec<"/users/:groupId", {
groupId: string | number | boolean;
}, {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}, never>
users >;
type type UsersSearchValues = {
keyword: string | undefined;
page: number;
}
UsersSearchValues = type InferRouteSpecSearchValues<TSpec> = RouteSpecAssociatedTypesOf<TSpec> extends {
searchValues: infer TValues;
} ? TValues : never
提取 RouteSpec 从 URL 解码 search params 后得到的业务值类型。
这是从 URL 读取数据的结果类型。它会反映 codec 的 optional 和 defaultValue
语义:可选字段可能是 undefined,具有默认值的字段则是必需值。它适合用于
页面状态、组件 props,以及消费 useSearchValues 解码结果的业务函数。
它不同于
InferRouteSpecSearchInput
:后者是生成 URL 时接受的 partial
输入,因此字段可选并可能接受 nullish 值。
@exampleconst orders = routeSpec({
path: '/orders',
search: ordersSearch,
})
type OrdersSearchValues = InferRouteSpecSearchValues<typeof orders>
type OrdersTableProps = {
search: OrdersSearchValues
}
function OrdersPage() {
const [search] = useSearchValues(orders.search)
return <OrdersTable search={search} />
}
InferRouteSpecSearchValues <typeof const users: RouteSpec<"/users/:groupId", {
groupId: string | number | boolean;
}, {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
}, never>
users >;#InferRouteSpecParams
提取生成 pathname 所需的 path params:
type UsersParams = {
groupId: string | number | boolean
}#InferRouteSpecSearchInput
提取生成 href 时接受的 partial search input:
type UsersSearchInput = {
keyword?: string | null | undefined
page?: number | null | undefined
}它描述写入方向,因此所有字段可选,并保留 nullish 输入语义。
#InferRouteSpecSearchValues
提取 URL decode 后的完整业务值:
type UsersSearchValues = {
keyword: string | undefined
page: number
}它描述读取方向,会反映 optional Field 与 defaultValue 的类型差异。
#能力边界
- RouteSpec 只生成应用内部 href string,不补充 React Router basename。
- RouteSpec 不匹配 pathname,也不解析 path params;页面继续使用 React Router
useParams。 - RouteSpec 不定义 parent/child route composition,也不替代 router config。
- RouteSpec 不生成包含 origin 的完整 URL。
- Hash contract 属于 TODO,当前不作为 RouteSpec API 描述。
- Navigation state contract 属于 TODO,当前不作为 RouteSpec API 描述。