You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript:为带const断言的路由数组创建递归函数参数类型

为带const断言的递归路由数组注入element字段的类型问题

路由数组与类型定义

我有如下结构的路由数组,注意:as const断言无法移除,因为其他代码依赖它:

const routes = [
  {
    id: 'root',
    path: '/',
    children: [
      {
        id: 'auth',
        path: 'auth',
        children: [
          { id: 'login', path: 'login', },
          { id: 'register', path: 'register', },
          { id: 'resetPassword', path: 'reset-password', },
          { id: 'resendConfirmation', path: 'resend-confirmation', },
        ],
      },
      {
        id: 'playground',
        path: 'playground',
        children: [
          { id: 'formControlsPlayground', path: 'form-controls', },
        ],
      },
    ],
  },
] as const;

我将该数组的类型提取为:

type Routes = typeof routes;

注入element字段的函数需求

我需要编写一个injectElement函数,为每个路由注入element字段(实际会通过路由的id从routeComponentLookup中获取对应组件,示例中用字符串代替JSX)。函数返回RouteObject类型,定义如下:

type RouteObject = {
  id?: string;
  path?: string;
  element?: string;
  children?: RouteObject[];
}

尝试编写的函数如下,但无法传入routes[0].children进行递归调用,需要为函数参数创建递归类型,且保留const断言带来的字面量类型,以便通过id索引routeComponentLookup:

// 手动定义Id类型仅为简化示例,实际会自动从路由数组生成
type Id = "root" | "auth" | "login" | "register" | "resetPassword" | "resendConfirmation" |
  "playground" | "formControlsPlayground"

declare const routeComponentLookup: Record<Id, string>;

const injectElement = (routes: Routes) => {
  const injectedRoutes: RouteObject[] = [];
  for (let route of routes) {
    injectedRoutes.push({
      path: route.path,
      element: routeComponentLookup[route.id],
      children: route.children ? injectElement(route.children) : undefined, // 此处报错:类型不匹配
    });
  }
  return injectedRoutes;
};

console.log(injectElement(routes));

解决方法

1. 定义递归路由项类型

首先定义描述单个路由项的递归类型,让TypeScript能识别嵌套的路由结构:

type RouteItem<T extends readonly any[]> = {
  readonly id: string;
  readonly path: string;
  readonly children?: T;
};

type RecursiveRouteArray = readonly RouteItem<RecursiveRouteArray>[];

2. 调整函数参数为通用递归类型

将injectElement的参数改为通用递归类型,这样TypeScript就能正确推断route.children的类型,允许递归调用:

const injectElement = <T extends RecursiveRouteArray>(routes: T) => {
  const injectedRoutes: RouteObject[] = [];
  for (const route of routes) {
    injectedRoutes.push({
      path: route.path,
      element: routeComponentLookup[route.id as Id], // 若Id自动生成可省略此断言
      children: route.children ? injectElement(route.children) : undefined,
    });
  }
  return injectedRoutes;
};

3. 自动生成Id类型(可选优化)

如果不想手动维护Id类型,可以通过递归类型自动从路由数组中提取所有id的字面量:

type ExtractRouteIds<T extends RecursiveRouteArray> = 
  T[number] extends infer R 
    ? R extends RouteItem<infer C> 
      ? R['id'] | (C extends RecursiveRouteArray ? ExtractRouteIds<C> : never)
      : never
    : never;

type AutoGeneratedId = ExtractRouteIds<typeof routes>;
// AutoGeneratedId 会自动推导为所有路由id的字面量联合类型

declare const routeComponentLookup: Record<AutoGeneratedId, string>;

调整后,递归调用injectElement(route.children)不再报错,同时保留了const断言带来的字面量类型安全,确保route.id能正确索引routeComponentLookup。

内容的提问来源于stack exchange,提问作者Vishal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 16:49:53