Appearance
类型安全路由
概述
类型安全路由通过TypeScript的强大类型系统,在编译时捕获路由相关的错误,提供完整的类型推断和IDE智能提示。本文深入探讨如何在React应用中实现完全类型安全的路由系统,涵盖路径参数、查询参数、导航等各个方面。
为什么需要类型安全路由
常见的路由类型错误
typescript
// 传统路由的问题
// 问题1:路径拼写错误(运行时才会发现)
navigate('/usres/123'); // 拼写错误:usres -> users
// 问题2:参数类型不匹配
<Link to={`/users/${null}`}>User</Link> // null参数
// 问题3:缺少必需参数
navigate('/products'); // 缺少productId参数
// 问题4:查询参数类型不安全
const page = searchParams.get('page'); // 返回string | null
const numPage = parseInt(page); // 可能是NaN
// 问题5:使用不存在的参数
const { nonExistentParam } = useParams(); // 没有类型检查类型安全的优势
typescript
const typeSafetyAdvantages = {
compileTimeErrors: {
description: '编译时发现错误,而不是运行时',
example: '路径拼写错误会立即标红'
},
autoComplete: {
description: 'IDE智能提示路径和参数',
example: '输入时自动补全可用的路由路径'
},
refactoringSafety: {
description: '重构时自动更新所有引用',
example: '重命名路由时IDE会提示所有使用位置'
},
documentationInCode: {
description: '类型即文档',
example: '参数类型和必需性一目了然'
},
preventBugs: {
description: '预防常见bug',
example: '防止传递错误类型的参数'
}
};基础类型安全实现
路由类型定义
typescript
// routes/types.ts
// 定义应用的所有路由
// 路由路径类型
export type RoutePath =
| '/'
| '/about'
| '/users'
| '/users/:userId'
| '/users/:userId/posts'
| '/users/:userId/posts/:postId'
| '/products'
| '/products/:productId'
| '/search'
| '/settings'
| '/settings/:section';
// 路径参数类型映射
export interface RouteParams {
'/': never;
'/about': never;
'/users': never;
'/users/:userId': { userId: string };
'/users/:userId/posts': { userId: string };
'/users/:userId/posts/:postId': { userId: string; postId: string };
'/products': never;
'/products/:productId': { productId: string };
'/search': never;
'/settings': never;
'/settings/:section': { section: string };
}
// 查询参数类型映射
export interface RouteSearch {
'/': never;
'/about': never;
'/users': {
page?: number;
limit?: number;
sortBy?: 'name' | 'date' | 'activity';
};
'/users/:userId': never;
'/users/:userId/posts': {
page?: number;
};
'/users/:userId/posts/:postId': never;
'/products': {
category?: string;
minPrice?: number;
maxPrice?: number;
page?: number;
};
'/products/:productId': never;
'/search': {
q: string;
category?: string;
page?: number;
};
'/settings': never;
'/settings/:section': never;
}
// 辅助类型:提取参数名
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<Rest>]: string }
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: never;
// 辅助类型:是否有参数
type HasParams<T extends RoutePath> =
T extends keyof RouteParams
? RouteParams[T] extends never
? false
: true
: false;
// 辅助类型:是否有查询参数
type HasSearch<T extends RoutePath> =
T extends keyof RouteSearch
? RouteSearch[T] extends never
? false
: true
: false;类型安全的Link组件
typescript
// components/TypedLink.tsx
import { Link as RouterLink, LinkProps } from 'react-router-dom';
import { RoutePath, RouteParams, RouteSearch } from '../routes/types';
// 类型安全的Link Props
type TypedLinkProps<T extends RoutePath> = Omit<LinkProps, 'to'> & {
to: T;
} & (RouteParams[T] extends never
? { params?: never }
: { params: RouteParams[T] }
) & (RouteSearch[T] extends never
? { search?: never }
: RouteSearch[T] extends Record<string, any>
? { search?: RouteSearch[T] }
: { search?: never }
);
export function TypedLink<T extends RoutePath>(
props: TypedLinkProps<T>
): JSX.Element {
const { to, params, search, ...rest } = props;
// 构建完整的路径
let path: string = to;
// 替换路径参数
if (params) {
Object.entries(params).forEach(([key, value]) => {
path = path.replace(`:${key}`, String(value));
});
}
// 添加查询参数
if (search) {
const searchParams = new URLSearchParams();
Object.entries(search).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.set(key, String(value));
}
});
const searchString = searchParams.toString();
if (searchString) {
path += `?${searchString}`;
}
}
return <RouterLink to={path} {...rest} />;
}
// 使用示例
function Navigation() {
return (
<nav>
{/* ✅ 正确:没有参数的路由 */}
<TypedLink to="/">Home</TypedLink>
{/* ✅ 正确:带必需参数 */}
<TypedLink
to="/users/:userId"
params={{ userId: '123' }}
>
User 123
</TypedLink>
{/* ✅ 正确:带查询参数 */}
<TypedLink
to="/users"
search={{ page: 1, sortBy: 'name' }}
>
Users
</TypedLink>
{/* ✅ 正确:同时有路径参数和查询参数 */}
<TypedLink
to="/users/:userId/posts"
params={{ userId: '123' }}
search={{ page: 2 }}
>
User Posts
</TypedLink>
{/* ❌ 错误:缺少必需的参数 */}
{/* <TypedLink to="/users/:userId">User</TypedLink> */}
{/* ❌ 错误:参数类型不匹配 */}
{/* <TypedLink
to="/users/:userId"
params={{ userId: 123 }}
/> */}
{/* ❌ 错误:不存在的查询参数 */}
{/* <TypedLink
to="/users"
search={{ invalid: 'param' }}
/> */}
</nav>
);
}类型安全的useNavigate
typescript
// hooks/useTypedNavigate.ts
import { useNavigate as useRouterNavigate } from 'react-router-dom';
import { RoutePath, RouteParams, RouteSearch } from '../routes/types';
type NavigateOptions<T extends RoutePath> = {
replace?: boolean;
state?: any;
} & (RouteParams[T] extends never
? { params?: never }
: { params: RouteParams[T] }
) & (RouteSearch[T] extends never
? { search?: never }
: RouteSearch[T] extends Record<string, any>
? { search?: RouteSearch[T] }
: { search?: never }
);
export function useTypedNavigate() {
const navigate = useRouterNavigate();
return <T extends RoutePath>(
to: T,
options?: NavigateOptions<T>
) => {
let path: string = to;
// 替换路径参数
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
path = path.replace(`:${key}`, String(value));
});
}
// 添加查询参数
if (options?.search) {
const searchParams = new URLSearchParams();
Object.entries(options.search).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.set(key, String(value));
}
});
const searchString = searchParams.toString();
if (searchString) {
path += `?${searchString}`;
}
}
navigate(path, {
replace: options?.replace,
state: options?.state
});
};
}
// 使用示例
function UserActions() {
const navigate = useTypedNavigate();
const handleViewUser = (userId: string) => {
// ✅ 类型安全
navigate('/users/:userId', {
params: { userId }
});
};
const handleSearch = (query: string) => {
// ✅ 类型安全
navigate('/search', {
search: { q: query, page: 1 }
});
};
const handleError = () => {
// ❌ 编译错误:缺少必需参数
// navigate('/users/:userId');
// ❌ 编译错误:参数类型错误
// navigate('/users/:userId', { params: { userId: 123 } });
};
return (
<div>
<button onClick={() => handleViewUser('123')}>
View User
</button>
<button onClick={() => handleSearch('react')}>
Search
</button>
</div>
);
}类型安全的useParams
typescript
// hooks/useTypedParams.ts
import { useParams } from 'react-router-dom';
import { RouteParams } from '../routes/types';
export function useTypedParams<T extends keyof RouteParams>(): RouteParams[T] {
const params = useParams();
return params as RouteParams[T];
}
// 使用示例
function UserDetail() {
// ✅ 完全类型安全
const { userId } = useTypedParams<'/users/:userId'>();
// userId的类型是 string
return (
<div>
<h1>User ID: {userId}</h1>
</div>
);
}
function PostDetail() {
// ✅ 多个参数也类型安全
const { userId, postId } = useTypedParams<'/users/:userId/posts/:postId'>();
// userId和postId都是 string
return (
<div>
<h1>User: {userId}</h1>
<h2>Post: {postId}</h2>
</div>
);
}类型安全的useSearchParams
typescript
// hooks/useTypedSearchParams.ts
import { useSearchParams } from 'react-router-dom';
import { RouteSearch } from '../routes/types';
export function useTypedSearchParams<T extends keyof RouteSearch>() {
const [searchParams, setSearchParams] = useSearchParams();
// 解析查询参数为类型安全的对象
const typedSearch = Object.fromEntries(searchParams) as RouteSearch[T];
// 类型安全的更新函数
const setTypedSearch = (
updates: Partial<RouteSearch[T]> | ((prev: RouteSearch[T]) => Partial<RouteSearch[T]>)
) => {
const newSearch = typeof updates === 'function'
? updates(typedSearch)
: updates;
const newParams = new URLSearchParams();
Object.entries(newSearch).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
newParams.set(key, String(value));
}
});
setSearchParams(newParams);
};
return [typedSearch, setTypedSearch] as const;
}
// 使用示例
function UsersList() {
const [search, setSearch] = useTypedSearchParams<'/users'>();
// ✅ 类型安全的查询参数
const page = search.page ?? 1;
const sortBy = search.sortBy ?? 'name';
const handlePageChange = (newPage: number) => {
// ✅ 类型检查
setSearch({ ...search, page: newPage });
};
const handleSortChange = (sortBy: 'name' | 'date' | 'activity') => {
// ✅ 类型检查,只能使用定义的值
setSearch({ ...search, sortBy });
};
return (
<div>
<div>Current page: {page}</div>
<div>Sort by: {sortBy}</div>
<button onClick={() => handlePageChange(page + 1)}>
Next Page
</button>
<select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value as any)}
>
<option value="name">Name</option>
<option value="date">Date</option>
<option value="activity">Activity</option>
</select>
</div>
);
}
function ProductsFilter() {
const [search, setSearch] = useTypedSearchParams<'/products'>();
return (
<div className="filters">
<input
type="text"
placeholder="Category"
value={search.category ?? ''}
onChange={(e) => setSearch({ ...search, category: e.target.value })}
/>
<input
type="number"
placeholder="Min Price"
value={search.minPrice ?? ''}
onChange={(e) => setSearch({
...search,
minPrice: parseFloat(e.target.value)
})}
/>
<input
type="number"
placeholder="Max Price"
value={search.maxPrice ?? ''}
onChange={(e) => setSearch({
...search,
maxPrice: parseFloat(e.target.value)
})}
/>
</div>
);
}高级类型安全模式
路由配置生成器
typescript
// utils/createRoute.ts
import { RouteObject } from 'react-router-dom';
import { RoutePath, RouteParams, RouteSearch } from '../routes/types';
interface RouteConfig<T extends RoutePath> {
path: T;
element: React.ReactElement;
loader?: (params: RouteParams[T], search: RouteSearch[T]) => Promise<any>;
action?: (params: RouteParams[T], formData: FormData) => Promise<any>;
errorElement?: React.ReactElement;
children?: RouteConfig<any>[];
}
export function createRoute<T extends RoutePath>(
config: RouteConfig<T>
): RouteObject {
return {
path: config.path,
element: config.element,
errorElement: config.errorElement,
loader: config.loader ? async ({ params, request }) => {
const url = new URL(request.url);
const search = Object.fromEntries(url.searchParams) as RouteSearch[T];
return config.loader!(params as RouteParams[T], search);
} : undefined,
action: config.action ? async ({ params, request }) => {
const formData = await request.formData();
return config.action!(params as RouteParams[T], formData);
} : undefined,
children: config.children?.map(child => createRoute(child))
};
}
// 使用示例
const routes = [
createRoute({
path: '/',
element: <HomePage />
}),
createRoute({
path: '/users/:userId',
element: <UserDetail />,
loader: async (params, search) => {
// params的类型是 { userId: string }
const user = await fetchUser(params.userId);
return { user };
}
}),
createRoute({
path: '/search',
element: <SearchPage />,
loader: async (params, search) => {
// search的类型是 { q: string; category?: string; page?: number }
const results = await searchAPI(search.q, {
category: search.category,
page: search.page ?? 1
});
return { results };
}
})
];Zod集成
typescript
// routes/schemas.ts
import { z } from 'zod';
// 定义参数Schema
export const userIdSchema = z.object({
userId: z.string().uuid()
});
export const postParamsSchema = z.object({
userId: z.string().uuid(),
postId: z.string().uuid()
});
export const productsSearchSchema = z.object({
category: z.string().optional(),
minPrice: z.number().min(0).optional(),
maxPrice: z.number().max(10000).optional(),
page: z.number().int().positive().default(1)
});
export const searchQuerySchema = z.object({
q: z.string().min(1),
category: z.string().optional(),
page: z.number().int().positive().default(1)
});
// 类型推断
export type UserIdParams = z.infer<typeof userIdSchema>;
export type PostParams = z.infer<typeof postParamsSchema>;
export type ProductsSearch = z.infer<typeof productsSearchSchema>;
export type SearchQuery = z.infer<typeof searchQuerySchema>;
// 验证Hook
export function useValidatedParams<T extends z.ZodType>(schema: T): z.infer<T> {
const params = useParams();
const result = schema.safeParse(params);
if (!result.success) {
throw new Error(`Invalid params: ${result.error.message}`);
}
return result.data;
}
export function useValidatedSearch<T extends z.ZodType>(schema: T): z.infer<T> {
const [searchParams] = useSearchParams();
const search = Object.fromEntries(searchParams);
// 转换数字类型
Object.keys(search).forEach(key => {
const value = search[key];
if (!isNaN(Number(value))) {
search[key] = Number(value);
}
});
const result = schema.safeParse(search);
if (!result.success) {
throw new Error(`Invalid search params: ${result.error.message}`);
}
return result.data;
}
// 使用示例
function UserDetail() {
// ✅ 参数经过验证且类型安全
const { userId } = useValidatedParams(userIdSchema);
// userId是经过UUID验证的string
return <div>User: {userId}</div>;
}
function SearchResults() {
// ✅ 查询参数经过验证且类型安全
const search = useValidatedSearch(searchQuerySchema);
// search.q: string (必需)
// search.category?: string
// search.page: number (默认1)
return (
<div>
<h1>Results for: {search.q}</h1>
<p>Page: {search.page}</p>
</div>
);
}路由权限类型
typescript
// types/permissions.ts
export type Permission =
| 'users:read'
| 'users:write'
| 'posts:read'
| 'posts:write'
| 'admin:access'
| 'reports:view';
export type Role = 'admin' | 'moderator' | 'user' | 'guest';
export interface User {
id: string;
name: string;
role: Role;
permissions: Permission[];
}
// 路由权限映射
export interface RoutePermissions {
'/': never;
'/about': never;
'/users': 'users:read';
'/users/:userId': 'users:read';
'/users/:userId/edit': 'users:write';
'/admin': 'admin:access';
'/reports': 'reports:view';
}
// 类型安全的权限检查Hook
export function useRequirePermission<T extends keyof RoutePermissions>(
route: T
): boolean {
const user = useUser();
const requiredPermission = RoutePermissions[route];
if (requiredPermission === never) {
return true; // 公开路由
}
return user?.permissions.includes(requiredPermission as Permission) ?? false;
}
// 权限守卫组件
function PermissionGuard<T extends keyof RoutePermissions>({
route,
children,
fallback
}: {
route: T;
children: React.ReactNode;
fallback?: React.ReactNode;
}) {
const hasPermission = useRequirePermission(route);
if (!hasPermission) {
return <>{fallback || <Navigate to="/unauthorized" />}</>;
}
return <>{children}</>;
}
// 使用示例
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/users"
element={
<PermissionGuard route="/users">
<Users />
</PermissionGuard>
}
/>
<Route
path="/users/:userId/edit"
element={
<PermissionGuard route="/users/:userId/edit">
<EditUser />
</PermissionGuard>
}
/>
</Routes>
);
}实战应用
完整的类型安全路由系统
typescript
// routes/index.ts
// 完整的类型安全路由配置
import { z } from 'zod';
import { createRoute } from '../utils/createRoute';
// 1. 定义所有Schema
const schemas = {
params: {
userId: z.object({ userId: z.string().uuid() }),
postId: z.object({
userId: z.string().uuid(),
postId: z.string().uuid()
}),
productId: z.object({ productId: z.string() })
},
search: {
pagination: z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().positive().default(10)
}),
products: z.object({
category: z.string().optional(),
minPrice: z.number().min(0).optional(),
maxPrice: z.number().max(10000).optional(),
sortBy: z.enum(['name', 'price', 'rating']).default('name'),
page: z.number().int().positive().default(1)
}),
search: z.object({
q: z.string().min(1),
category: z.string().optional(),
page: z.number().int().positive().default(1)
})
}
};
// 2. 导出类型
export type UserId = z.infer<typeof schemas.params.userId>;
export type PostId = z.infer<typeof schemas.params.postId>;
export type ProductsSearch = z.infer<typeof schemas.search.products>;
export type SearchQuery = z.infer<typeof schemas.search.search>;
// 3. 创建类型安全的路由配置
export const routes = [
createRoute({
path: '/',
element: <HomePage />,
loader: async () => {
const featured = await fetchFeaturedContent();
return { featured };
}
}),
createRoute({
path: '/users',
element: <UsersLayout />,
children: [
{
path: '',
element: <UsersList />,
loader: async (params, search) => {
const validated = schemas.search.pagination.parse(search);
const users = await fetchUsers(validated);
return { users };
}
},
{
path: ':userId',
element: <UserDetail />,
loader: async (params, search) => {
const { userId } = schemas.params.userId.parse(params);
const user = await fetchUser(userId);
if (!user) {
throw new Response('User not found', { status: 404 });
}
return { user };
}
},
{
path: ':userId/posts/:postId',
element: <PostDetail />,
loader: async (params, search) => {
const { userId, postId } = schemas.params.postId.parse(params);
const [post, author] = await Promise.all([
fetchPost(postId),
fetchUser(userId)
]);
if (!post || !author) {
throw new Response('Not found', { status: 404 });
}
return { post, author };
}
}
]
}),
createRoute({
path: '/products',
element: <ProductsLayout />,
children: [
{
path: '',
element: <ProductsList />,
loader: async (params, search) => {
const filters = schemas.search.products.parse(search);
const products = await fetchProducts(filters);
const categories = await fetchCategories();
return { products, categories, filters };
}
},
{
path: ':productId',
element: <ProductDetail />,
loader: async (params, search) => {
const { productId } = schemas.params.productId.parse(params);
const product = await fetchProduct(productId);
if (!product) {
throw new Response('Product not found', { status: 404 });
}
return { product };
}
}
]
}),
createRoute({
path: '/search',
element: <SearchPage />,
loader: async (params, search) => {
const query = schemas.search.search.parse(search);
const results = await searchAPI(query.q, {
category: query.category,
page: query.page
});
return { results, query };
}
})
];
// 4. 类型安全的组件
function ProductsList() {
const { products, filters } = useLoaderData() as {
products: Product[];
filters: ProductsSearch;
};
const navigate = useTypedNavigate();
const updateFilters = (newFilters: Partial<ProductsSearch>) => {
navigate('/products', {
search: { ...filters, ...newFilters }
});
};
return (
<div>
<ProductsFilter
filters={filters}
onChange={updateFilters}
/>
<div className="products-grid">
{products.map(product => (
<TypedLink
key={product.id}
to="/products/:productId"
params={{ productId: product.id }}
>
<ProductCard product={product} />
</TypedLink>
))}
</div>
</div>
);
}路由生成器和辅助函数
typescript
// utils/routes.ts
// 路由URL生成器
import { RoutePath, RouteParams, RouteSearch } from '../routes/types';
// 类型安全的URL生成器
export function generatePath<T extends RoutePath>(
path: T,
...args: RouteParams[T] extends never
? []
: [params: RouteParams[T]]
): string {
let result: string = path;
if (args.length > 0) {
const params = args[0] as Record<string, string>;
Object.entries(params).forEach(([key, value]) => {
result = result.replace(`:${key}`, value);
});
}
return result;
}
// 类型安全的查询字符串生成器
export function generateSearch<T extends RoutePath>(
path: T,
search: RouteSearch[T]
): string {
if (!search || Object.keys(search).length === 0) {
return '';
}
const params = new URLSearchParams();
Object.entries(search).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.set(key, String(value));
}
});
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
// 完整URL生成器
export function generateUrl<T extends RoutePath>(
path: T,
options?: {
params?: RouteParams[T];
search?: RouteSearch[T];
}
): string {
const pathname = options?.params
? generatePath(path, options.params as any)
: path;
const searchString = options?.search
? generateSearch(path, options.search)
: '';
return pathname + searchString;
}
// 使用示例
const urls = {
home: generateUrl('/'),
// 结果: "/"
user: generateUrl('/users/:userId', {
params: { userId: '123' }
}),
// 结果: "/users/123"
userPosts: generateUrl('/users/:userId/posts', {
params: { userId: '123' },
search: { page: 2 }
}),
// 结果: "/users/123/posts?page=2"
products: generateUrl('/products', {
search: {
category: 'electronics',
minPrice: 100,
maxPrice: 500,
page: 1
}
}),
// 结果: "/products?category=electronics&minPrice=100&maxPrice=500&page=1"
search: generateUrl('/search', {
search: {
q: 'react router',
page: 1
}
})
// 结果: "/search?q=react+router&page=1"
};路由测试工具
typescript
// utils/testing.tsx
// 类型安全的路由测试工具
import { render } from '@testing-library/react';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import { RoutePath, RouteParams, RouteSearch } from '../routes/types';
interface RenderRouteOptions<T extends RoutePath> {
route: T;
params?: RouteParams[T];
search?: RouteSearch[T];
initialEntries?: string[];
}
export function renderRoute<T extends RoutePath>(
component: React.ReactElement,
options: RenderRouteOptions<T>
) {
const { route, params, search, initialEntries } = options;
let path = route as string;
// 替换参数
if (params) {
Object.entries(params).forEach(([key, value]) => {
path = path.replace(`:${key}`, String(value));
});
}
// 添加查询参数
if (search) {
const searchParams = new URLSearchParams();
Object.entries(search).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.set(key, String(value));
}
});
const searchString = searchParams.toString();
if (searchString) {
path += `?${searchString}`;
}
}
const router = createMemoryRouter(
[
{
path: route,
element: component
}
],
{
initialEntries: initialEntries || [path]
}
);
return render(<RouterProvider router={router} />);
}
// 测试示例
describe('UserDetail', () => {
it('renders user information', () => {
const { getByText } = renderRoute(<UserDetail />, {
route: '/users/:userId',
params: { userId: '123' }
});
expect(getByText(/User ID: 123/)).toBeInTheDocument();
});
});
describe('ProductsList', () => {
it('applies filters from search params', () => {
const { getByDisplayValue } = renderRoute(<ProductsList />, {
route: '/products',
search: {
category: 'electronics',
minPrice: 100,
page: 1
}
});
expect(getByDisplayValue('electronics')).toBeInTheDocument();
expect(getByDisplayValue('100')).toBeInTheDocument();
});
});最佳实践
1. 类型定义组织
typescript
// types/routes/
// ├── index.ts // 导出所有类型
// ├── paths.ts // 路径定义
// ├── params.ts // 参数类型
// ├── search.ts // 查询参数类型
// └── permissions.ts // 权限类型
// types/routes/index.ts
export * from './paths';
export * from './params';
export * from './search';
export * from './permissions';
// types/routes/paths.ts
export type RoutePath =
| '/'
| '/users'
| '/users/:userId'
// ... 更多路径
// types/routes/params.ts
export interface RouteParams {
'/': never;
'/users': never;
'/users/:userId': { userId: string };
// ... 更多参数
}
// types/routes/search.ts
export interface RouteSearch {
'/': never;
'/users': UserListSearch;
'/products': ProductsSearch;
// ... 更多查询参数
}
interface UserListSearch {
page?: number;
limit?: number;
sortBy?: 'name' | 'date';
}
interface ProductsSearch {
category?: string;
minPrice?: number;
maxPrice?: number;
}2. 渐进式采用
typescript
// 步骤1:首先为关键路由添加类型
const criticalRoutes = [
'/users/:userId',
'/products/:productId',
'/checkout'
] as const;
// 步骤2:逐步扩展到所有路由
// 步骤3:添加查询参数类型
// 步骤4:添加权限类型
// 步骤5:完善错误处理3. 性能优化
typescript
// 使用类型常量避免重复计算
const ROUTE_PATHS = {
HOME: '/' as const,
USERS: '/users' as const,
USER_DETAIL: '/users/:userId' as const,
// ...
} as const;
type RoutePathValue = typeof ROUTE_PATHS[keyof typeof ROUTE_PATHS];
// 缓存生成的URL
const urlCache = new Map<string, string>();
function cachedGenerateUrl<T extends RoutePath>(
path: T,
options?: any
): string {
const cacheKey = JSON.stringify({ path, options });
if (urlCache.has(cacheKey)) {
return urlCache.get(cacheKey)!;
}
const url = generateUrl(path, options);
urlCache.set(cacheKey, url);
return url;
}4. 文档和维护
typescript
// routes/README.md
/**
* 路由系统文档
*
* ## 添加新路由
*
* 1. 在 types/routes/paths.ts 中添加路径
* 2. 在 types/routes/params.ts 中定义参数类型
* 3. 在 types/routes/search.ts 中定义查询参数类型
* 4. 在 routes/index.ts 中添加路由配置
* 5. 确保所有类型测试通过
*
* ## 类型安全检查清单
*
* - [ ] 路径拼写正确
* - [ ] 参数类型完整
* - [ ] 查询参数类型完整
* - [ ] 权限配置正确
* - [ ] 测试覆盖完整
*/总结
类型安全路由提供了强大的保障:
- 编译时错误检测:在编译阶段发现路由错误
- IDE智能提示:完整的自动补全和类型提示
- 重构安全:重命名和修改时自动更新引用
- 类型文档:类型定义即文档
- 更好的开发体验:减少运行时错误,提高开发效率
类型安全路由特别适合大型TypeScript项目,能够显著提升代码质量和维护性。虽然初期需要一些额外的类型定义工作,但长期来看能够大幅减少bug和提高开发效率。