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

TypeScript:生成RouteUrl类型时遇类型错误求助

TypeScript 实现路径参数替换的 RouteUrl 类型问题

已实现的路径参数提取类型

/**
 * 按 `/` 分割输入路径,返回路径段的联合类型。
 * 使用尾递归条件类型处理长路径的性能问题
 *
 * @example
 * ```
 * type Segments = PathSegments<'/courses/:courseId/classes/:classId'>;
 * //   ^? "" | "courses" | ":courseId" | "classes" | ":classId"
 * ```
 */
type PathSegments<
  Path extends string,
  Accumulator extends string = never
> = Path extends `${infer Left}/${infer Right}`
  ? PathSegments<Right, Accumulator | Left>
  : Accumulator | Path;

/**
 * 创建一个对象类型:
 * - 键为 PathSegments 中以 `:` 开头的路径段(去掉:`)
 * - 值为 string 类型
 *
 * 使用键重映射过滤掉不以:`开头的路径段
 *
 * @example 1 - 路径无动态参数
 * ```
 * type Params = RouteParams<'/courses/classes'>;
 * //   ^? {}
 * ```
 *
 * @example 2 - 路径含动态参数
 * ```
 * type Params = RouteParams<'/courses/:courseId/classes/:classId'>;
 * //   ^? { courseId: string; classId: string }
 * ```
 */
type RouteParams<Path extends string> = {
  [PathSegment in PathSegments<Path> as PathSegment extends `:${infer RouteParam}`
    ? RouteParam
    : never]: string;
};

尝试实现 RouteUrl 类型及错误

尝试编写RouteUrl类型将路径中的参数替换为给定的Params值,但出现类型错误:

type RouteUrl<
  Path extends string,
  Params extends RouteParams<Path>
> = Path extends `${infer Left}/:${infer Param}/${infer Right}`
  ? `${Left}/${Params[Param]}/${RouteUrl<Right, Params extends RouteParams<Right> ? Params : never>}`
  : Path extends `${infer Left}/:${infer Param}`
  ? `${Left}/${Params[Param]}`
  : Path;

测试代码:

type Url = RouteUrl<'/courses/:courseId/classes/:classId', { courseId: '123'; classId: '456' }>;

报错信息:

Type 'Params[Param]' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
Type 'Params[string]' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
Type 'RouteParams[string]' is not assignable to type 'string | number | bigint | boolean | null | undefined'.ts(2322)
Type 'Param' cannot be used to index type 'Params'.ts(2536)

替代思路:遍历参数键逐个替换

希望参考运行时的替换逻辑,在类型层面遍历Params的键,将路径中的:${key}替换为对应的值。已有字符串替换泛型:

export type Replace<
  Str extends string,
  Search extends string,
  Replacement extends string
> = Str extends `${infer Prefix}${Search}${infer Suffix}` ? `${Prefix}${Replacement}${Suffix}` : Str;

但不知道如何在类型层面遍历对象的键完成递归替换。


解决方案:递归遍历参数键实现替换

通过递归遍历Params的键联合类型,逐个调用Replace类型完成替换,直到所有参数处理完毕:

export type Replace<
  Str extends string,
  Search extends string,
  Replacement extends string
> = Str extends `${infer Prefix}${Search}${infer Suffix}` ? `${Prefix}${Replacement}${Suffix}` : Str;

type RouteUrl<
  Path extends string,
  Params extends Record<string, string>
> = keyof Params extends never
  ? Path
  : {
      [K in keyof Params]: RouteUrl<Replace<Path, `:${K & string}`, Params[K]>, Omit<Params, K>>;
    }[keyof Params];

测试验证

// 多参数路径测试
type Url1 = RouteUrl<'/courses/:courseId/classes/:classId', { courseId: '123'; classId: '456' }>;
//   ^? "/courses/123/classes/456"

// 无参数路径测试
type Url2 = RouteUrl<'/courses/classes', {}>;
//   ^? "/courses/classes"

// 单参数路径测试
type Url3 = RouteUrl<'/user/:userId', { userId: '789' }>;
//   ^? "/user/789"

逻辑说明

  1. 终止条件:当keyof Params为never(即Params为空对象),直接返回原路径。
  2. 递归替换:遍历Params的每个键K,先替换路径中的:${K}为对应值,再递归处理剩余参数(通过Omit<Params, K>移除已处理的键)。
  3. 结果收敛:利用映射类型的索引访问,从所有可能的替换分支中提取最终的单一字符串结果。

这个实现完美匹配运行时的循环替换逻辑,同时解决了原实现中TypeScript无法推断Param与Params键关联的问题。


内容的提问来源于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 04:34:53