TypeScript 配置

了解如何配置 @feoe/fs-router 的 TypeScript 支持,获得完整的类型安全体验。

基础 TypeScript 配置

tsconfig.json 配置

1{
2  "compilerOptions": {
3    "target": "ES2020",
4    "lib": ["DOM", "DOM.Iterable", "ES6"],
5    "allowJs": true,
6    "skipLibCheck": true,
7    "esModuleInterop": true,
8    "allowSyntheticDefaultImports": true,
9    "strict": true,
10    "forceConsistentCasingInFileNames": true,
11    "moduleResolution": "node",
12    "resolveJsonModule": true,
13    "isolatedModules": true,
14    "noEmit": true,
15    "jsx": "react-jsx",
16    
17    // 路径映射
18    "baseUrl": ".",
19    "paths": {
20      "@/*": ["./src/*"],
21      "@/routes": ["./src/routes.tsx"],
22      "@/routes-type": ["./src/routes-type.ts"]
23    }
24  },
25  "include": [
26    "src/**/*",
27    "src/routes.tsx",
28    "src/routes-type.ts"
29  ],
30  "exclude": [
31    "node_modules",
32    "dist"
33  ]
34}

插件 TypeScript 配置

1// vite.config.ts
2import { defineConfig } from 'vite'
3import { FileBasedRouterVite } from '@feoe/fs-router/vite'
4
5export default defineConfig({
6  plugins: [
7    FileBasedRouterVite({
8      routesDirectory: 'src/routes',
9      generatedRoutesPath: 'src/routes.tsx',
10      enableGeneration: true,
11      
12      // TypeScript 配置
13      typeGenerateOptions: {
14        routesTypeFile: 'src/routes-type.ts',
15        generateRouteParams: true,
16        generateLoaderTypes: true,
17        routesDirectories: []
18      }
19    })
20  ]
21})

类型生成选项

基础类型生成

1interface TypeGenerateOptions {
2  /** 类型文件输出路径 */
3  routesTypeFile: string
4  /** 是否生成路由参数类型 */
5  generateRouteParams?: boolean
6  /** 是否生成 Loader 类型 */
7  generateLoaderTypes?: boolean
8  /** 路由目录配置 */
9  routesDirectories?: RouteDirectory[]
10}
11
12interface RouteDirectory {
13  /** 路由前缀 */
14  prefix?: string
15  /** 路由目录路径 */
16  path: string
17}

生成的类型文件示例

1// 生成的 src/routes-type.ts
2export interface RouteParams {
3  '/': {}
4  '/about': {}
5  '/user': {}
6  '/user/:id': { id: string }
7  '/blog/:category/:slug': { 
8    category: string
9    slug: string 
10  }
11  '/docs/*': { '*': string }
12}
13
14export interface LoaderData {
15  '/user/:id': {
16    user: User
17    posts: Post[]
18  }
19  '/blog/:category/:slug': {
20    post: BlogPost
21    related: BlogPost[]
22  }
23}
24
25export interface ActionData {
26  '/user/:id': {
27    success: boolean
28    message: string
29  }
30}
31
32export interface RouteMeta {
33  '/admin/*': {
34    requiresAuth: true
35    roles: string[]
36  }
37}

路由参数类型

自动类型推断

1// src/routes/user/[id]/page.tsx
2import { useParams } from 'react-router-dom'
3
4export default function UserPage() {
5  // TypeScript 自动推断 id 为 string 类型
6  const { id } = useParams()
7  
8  return <div>用户 ID: {id}</div>
9}

手动类型定义

1// src/routes/blog/[category]/[slug]/page.tsx
2import { useParams } from 'react-router-dom'
3
4// 定义参数接口
5interface BlogParams {
6  category: string
7  slug: string
8}
9
10export default function BlogPostPage() {
11  const { category, slug } = useParams<BlogParams>()
12  
13  return (
14    <div>
15      <h1>分类: {category}</h1>
16      <h2>文章: {slug}</h2>
17    </div>
18  )
19}

复杂参数类型

1// src/routes/search/[[...filters]]/page.tsx
2import { useParams } from 'react-router-dom'
3
4interface SearchParams {
5  filters?: string[]
6}
7
8export default function SearchPage() {
9  const params = useParams<SearchParams>()
10  const filters = params.filters || []
11  
12  return (
13    <div>
14      <h1>搜索结果</h1>
15      <p>过滤器: {filters.join(', ')}</p>
16    </div>
17  )
18}

Loader 类型安全

基础 Loader 类型

1// src/routes/user/[id]/page.tsx
2import { useLoaderData } from 'react-router-dom'
3
4// 定义数据接口
5interface User {
6  id: string
7  name: string
8  email: string
9  avatar?: string
10}
11
12interface UserLoaderData {
13  user: User
14  posts: Post[]
15}
16
17// Loader 函数
18export async function loader({ params }): Promise<UserLoaderData> {
19  const [user, posts] = await Promise.all([
20    fetchUser(params.id),
21    fetchUserPosts(params.id)
22  ])
23  
24  return { user, posts }
25}
26
27// 组件中使用
28export default function UserPage() {
29  // TypeScript 自动推断数据类型
30  const { user, posts } = useLoaderData<typeof loader>()
31  
32  return (
33    <div>
34      <h1>{user.name}</h1>
35      <p>{user.email}</p>
36      <div>文章数量: {posts.length}</div>
37    </div>
38  )
39}

泛型 Loader

1// src/utils/loaders.ts
2export interface PaginatedResponse<T> {
3  data: T[]
4  total: number
5  page: number
6  pageSize: number
7}
8
9export async function createPaginatedLoader<T>(
10  fetcher: (page: number, pageSize: number) => Promise<PaginatedResponse<T>>
11) {
12  return async ({ request }): Promise<PaginatedResponse<T>> => {
13    const url = new URL(request.url)
14    const page = parseInt(url.searchParams.get('page') || '1')
15    const pageSize = parseInt(url.searchParams.get('pageSize') || '10')
16    
17    return fetcher(page, pageSize)
18  }
19}
20
21// 使用泛型 Loader
22export const loader = createPaginatedLoader<User>(fetchUsers)

Action 类型安全

基础 Action 类型

1// src/routes/user/[id]/edit/page.tsx
2import { redirect } from 'react-router-dom'
3
4interface UpdateUserData {
5  name: string
6  email: string
7  avatar?: string
8}
9
10interface ActionResult {
11  success: boolean
12  message: string
13  errors?: Record<string, string>
14}
15
16export async function action({ request, params }): Promise<ActionResult | Response> {
17  const formData = await request.formData()
18  
19  const userData: UpdateUserData = {
20    name: formData.get('name') as string,
21    email: formData.get('email') as string,
22    avatar: formData.get('avatar') as string || undefined
23  }
24  
25  try {
26    await updateUser(params.id, userData)
27    return redirect(`/user/${params.id}`)
28  } catch (error) {
29    return {
30      success: false,
31      message: '更新失败',
32      errors: { general: error.message }
33    }
34  }
35}
36
37export default function EditUserPage() {
38  const actionData = useActionData<typeof action>()
39  
40  return (
41    <form method="post">
42      {actionData && !actionData.success && (
43        <div className="error">{actionData.message}</div>
44      )}
45      {/* 表单字段 */}
46    </form>
47  )
48}

表单验证类型

1// src/utils/validation.ts
2export interface ValidationResult<T> {
3  success: boolean
4  data?: T
5  errors?: Partial<Record<keyof T, string>>
6}
7
8export function validateUserForm(formData: FormData): ValidationResult<UpdateUserData> {
9  const name = formData.get('name') as string
10  const email = formData.get('email') as string
11  
12  const errors: Partial<Record<keyof UpdateUserData, string>> = {}
13  
14  if (!name || name.length < 2) {
15    errors.name = '姓名至少需要2个字符'
16  }
17  
18  if (!email || !email.includes('@')) {
19    errors.email = '请输入有效的邮箱地址'
20  }
21  
22  if (Object.keys(errors).length > 0) {
23    return { success: false, errors }
24  }
25  
26  return {
27    success: true,
28    data: { name, email }
29  }
30}

错误类型处理

类型安全的错误处理

1// src/routes/error.tsx
2import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
3
4interface CustomError {
5  message: string
6  code?: string
7  details?: any
8}
9
10export default function ErrorBoundary() {
11  const error = useRouteError()
12  
13  // 类型守卫函数
14  function isCustomError(error: any): error is CustomError {
15    return error && typeof error.message === 'string'
16  }
17  
18  if (isRouteErrorResponse(error)) {
19    return (
20      <div className="error-page">
21        <h1>{error.status} {error.statusText}</h1>
22        <p>{error.data}</p>
23      </div>
24    )
25  }
26  
27  if (isCustomError(error)) {
28    return (
29      <div className="error-page">
30        <h1>应用错误</h1>
31        <p>{error.message}</p>
32        {error.code && <p>错误代码: {error.code}</p>}
33      </div>
34    )
35  }
36  
37  return (
38    <div className="error-page">
39      <h1>未知错误</h1>
40      <p>发生了意外错误</p>
41    </div>
42  )
43}

元数据类型

路由元数据类型定义

1// src/types/route-meta.ts
2export interface RouteMeta {
3  title?: string
4  description?: string
5  keywords?: string[]
6  requiresAuth?: boolean
7  roles?: string[]
8  layout?: 'default' | 'admin' | 'auth'
9  breadcrumbs?: BreadcrumbItem[]
10}
11
12export interface BreadcrumbItem {
13  label: string
14  path?: string
15}
16
17// 在路由中使用
18export const meta: RouteMeta = {
19  title: '用户管理',
20  description: '管理系统用户',
21  requiresAuth: true,
22  roles: ['admin'],
23  layout: 'admin',
24  breadcrumbs: [
25    { label: '首页', path: '/' },
26    { label: '管理', path: '/admin' },
27    { label: '用户管理' }
28  ]
29}

元数据访问 Hook

1// src/hooks/useRouteMeta.ts
2import { useMatches } from 'react-router-dom'
3import type { RouteMeta } from '@/types/route-meta'
4
5export function useRouteMeta(): RouteMeta {
6  const matches = useMatches()
7  
8  // 合并所有匹配路由的元数据
9  return matches.reduce((meta, match) => {
10    const routeMeta = match.handle?.meta as RouteMeta
11    return { ...meta, ...routeMeta }
12  }, {} as RouteMeta)
13}
14
15// 在组件中使用
16export default function PageHeader() {
17  const meta = useRouteMeta()
18  
19  return (
20    <header>
21      <h1>{meta.title}</h1>
22      {meta.description && <p>{meta.description}</p>}
23      {meta.breadcrumbs && (
24        <nav>
25          {meta.breadcrumbs.map((item, index) => (
26            <span key={index}>
27              {item.path ? (
28                <Link to={item.path}>{item.label}</Link>
29              ) : (
30                item.label
31              )}
32              {index < meta.breadcrumbs!.length - 1 && ' > '}
33            </span>
34          ))}
35        </nav>
36      )}
37    </header>
38  )
39}

高级类型特性

条件类型

1// src/types/route-utils.ts
2type RouteWithParams<T extends string> = T extends `${string}:${infer Param}${infer Rest}`
3  ? { [K in Param]: string } & RouteWithParams<Rest>
4  : {}
5
6type ExtractParams<T extends string> = RouteWithParams<T>
7
8// 使用示例
9type UserRouteParams = ExtractParams<'/user/:id/posts/:postId'>
10// 结果: { id: string; postId: string }

模板字面量类型

1// src/types/routes.ts
2type StaticRoutes = '/' | '/about' | '/contact'
3type UserRoutes = `/user/${string}`
4type AdminRoutes = `/admin/${string}`
5
6type AllRoutes = StaticRoutes | UserRoutes | AdminRoutes
7
8// 类型安全的导航函数
9function navigate<T extends AllRoutes>(path: T): void {
10  // 导航逻辑
11}
12
13// 使用
14navigate('/user/123')  // ✅ 正确
15navigate('/invalid')   // ❌ TypeScript 错误

开发工具集成

VS Code 配置

1// .vscode/settings.json
2{
3  "typescript.preferences.includePackageJsonAutoImports": "on",
4  "typescript.suggest.autoImports": true,
5  "typescript.updateImportsOnFileMove.enabled": "always",
6  "editor.codeActionsOnSave": {
7    "source.organizeImports": true
8  }
9}

ESLint TypeScript 规则

1// .eslintrc.js
2module.exports = {
3  extends: [
4    '@typescript-eslint/recommended',
5    '@typescript-eslint/recommended-requiring-type-checking'
6  ],
7  rules: {
8    '@typescript-eslint/no-unused-vars': 'error',
9    '@typescript-eslint/explicit-function-return-type': 'warn',
10    '@typescript-eslint/no-explicit-any': 'warn',
11    '@typescript-eslint/prefer-nullish-coalescing': 'error',
12    '@typescript-eslint/prefer-optional-chain': 'error'
13  }
14}

故障排除

常见类型错误

  1. 参数类型不匹配

    1// ❌ 错误
    2const { id } = useParams<{ id: number }>()
    3
    4// ✅ 正确 - 路由参数总是字符串
    5const { id } = useParams<{ id: string }>()
    6const numericId = parseInt(id)
  2. Loader 数据类型错误

    1// ❌ 错误
    2const data = useLoaderData() as UserData
    3
    4// ✅ 正确
    5const data = useLoaderData<typeof loader>()
  3. 可选参数处理

    1// ❌ 错误
    2const { category } = useParams<{ category: string }>()
    3
    4// ✅ 正确 - 可选参数可能为 undefined
    5const { category } = useParams<{ category?: string }>()

类型生成问题

  1. 类型文件未生成

    • 检查 enableGeneration 配置
    • 确认 routesTypeFile 路径正确
    • 验证文件写入权限
  2. 类型不准确

    • 检查路由文件导出
    • 确认参数命名规范
    • 验证 TypeScript 配置