TypeScript 模板字面量类型解析 URL 路径参数
在日常的前端开发中,我们经常需要处理 URL 路径参数,例如从 /users/123 中提取 id 为 123。传统做法通常是使用正则表达式或字符串分割,但在获取参数值后,类型信息往往会丢失,导致后续使用时缺乏安全提示。TypeScript 的模板字面量类型允许我们在编译期精确地解析路径结构,自动推导出参数的类型和名称。
1. 定义路径模板类型
首先,我们需要一种方式来描述 URL 的结构。TypeScript 允许我们使用字符串字面量来定义这些路径。我们将动态参数部分约定为以冒号 : 开头的字符串。
定义 一个基础的路由路径类型,包含静态部分和动态参数部分:
type UserPath = "/users/:id";
type PostPath = "/posts/:postId/comments/:commentId";
这里,:id、:postId 和 commentId 是我们希望提取的动态参数。
2. 构建参数提取工具类型
核心步骤是创建一个泛型类型 PathParams,它能够接收一个路径字符串,并返回一个包含所有参数及其类型的对象。我们需要利用 TypeScript 的条件类型和递归类型解析。
创建 PathParams 类型,逻辑如下:
- 检查字符串中是否包含模式
${string}:${string}/${string}`(即中间有参数)。 2. 如果匹配,提取冒号后面的参数名,并将剩余的路径继续递归解析。 3. 如果不匹配,检查是否包含 `${string}:${string}`(即末尾有参数)。 4. 如果匹配末尾参数,返回该参数。 5. 如果都不匹配,返回空对象。 在代码编辑器中**输入**以下类型定义: ```typescript type PathParams = T extends `${infer _}:${infer Param}/${infer Rest}
? { [K in Param]: string } & PathParams</${Rest}`> : T extends `${infer _}:${infer Param}` ? { [K in Param]: string } : {}; ``` **解析** 这个类型逻辑: * `infer _`:忽略参数名之前的路径部分。 * `:infer Param`:捕获参数名(例如 `id`)。 * `/${infer Rest}:捕获参数名之后的剩余路径,并带上/以便下一次递归匹配。
3. 验证类型推导结果
为了确保我们的类型定义正确,我们可以直接查看 TypeScript 推导出的类型结果。
声明几个变量来使用 PathParams:
type UserParams = PathParams<UserPath>;
type PostParams = PathParams<PostPath>;
type StaticParams = PathParams<"/home/profile">;
TypeScript 会自动推导出以下类型结构:
| 原始路径类型 | 推导出的参数类型 |
|---|---|
/users/:id |
{ id: string } |
/posts/:postId/comments/:commentId |
{ postId: string } & { commentId: string } |
/home/profile |
{} |
可以看到,无论是单参数还是多参数嵌套,类型都能被精确地解析出来。
4. 理解类型推断流程
为了更直观地理解 PathParams 的工作原理,下面的流程图展示了类型系统如何递归地解析复杂路径。
5. 实现运行时解析函数
仅有编译期的类型是不够的,我们需要一个运行时函数,它既能解析 URL,又能完美返回我们在第 2 步中定义的类型。
编写 parsePathParams 函数:
function parsePathParams<T extends string>(path: string, template: T): PathParams {
// 1. 将模板中的 :param 替换为正则捕获组 ([^/]+)
const regexPattern = template.replace(/:([^/]+)/g, '([^/]+)');
// 2. 构建正则对象,添加 ^ 和 $ 确保全匹配
const regex = new RegExp(`^${regexPattern}$`);
// 3. 执行匹配
const match = path.match(regex);
if (!match) {
return {} as PathParams;
}
// 4. 从模板中提取所有参数名
const keys = Array.from(template.matchAll(/:([^/]+)/g), m => m[1]);
// 5. 将参数名与匹配到的值组合成对象
const params = {} as PathParams;
keys.forEach((key, index) => {
// 类型断言:由于我们已经通过 PathParams 约束了结构,
// 这里可以安全地将 key 断言为 params 的键
(params as Record<string, string>)[key] = match[index + 1];
});
return params;
}
```
---
### 6. 使用与测试
现在,我们可以调用这个函数并享受完整的类型提示。
**执行**以下代码进行测试:
```typescript
const url1 = "/users/123";
const result1 = parsePathParams(url1, "/users/:id");
// 此时 result1.id 会自动提示为 string 类型
console.log(result1.id.toUpperCase());
const url2 = "/posts/99/comments/5";
const result2 = parsePathParams(url2, "/posts/:postId/comments/:commentId");
// result2 会包含 postId 和 commentId
console.log(`Post ID: ${result2.postId}, Comment ID: ${result2.commentId}`);
// 如果尝试访问不存在的属性,TypeScript 会报错
// console.log(result2.unknown); // Error: Property 'unknown' does not exist
当传入的 URL 路径与模板不匹配时,函数返回空对象,TypeScript 类型也会相应地变为 {} 或交叉类型的空集,确保了类型的安全性。

暂无评论,快来抢沙发吧!