「TS类型体操」✨通过一道类型体操检验你的体操基础

151 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情

不知道你的类型体操练得咋样了呢?通过下面这个类型体操来检验一下吧!

场景说明

url中的query params大家应该都不陌生吧?就是长这样子的:

a=1&a=2&b=3&d=4

而我们今天的目标,就是要通过类型体操,把这样的query params字符串转换成索引类型,并且同一个索引的不同值需要用数组保存

最终效果就是:

{
    a: ["1", "2"],
    b: "3",
    c: "4"
}

这涉及到多个类型体操的基本知识点,我们先来分析一下大概的完成类型体操的思路

思路分析

  • 首先我们需要把&分隔的每个param提取出来,也就是我们需要得到a=1b=3这样的项
  • 把提取出的每个param转成索引类型,也就是{ a: "1" }这样的形式
  • 对于同一个索引由不同值的,要转成数组存储,也就是{ a: ["1", "2"] }这样的形式

核心就是这三步了,那么每一步都应该怎么实现,涉及哪些知识点呢?

知识点分析

extends + infer 进行字符串模式匹配

对于第一点,需要按照&分割提取字符串,这就涉及到模式匹配的知识点了,可以通过extends条件类型进行约束判断,然后将匹配到的结果通过infer保存起来,不严谨地讲,可以理解为通过infer声明一个类型变量将我们想要的结果保存起来

通过递归模拟循环

由于一个query params中有多个键值对,所以我们应当循环遍历它们来构建我们的索引类型,那么如何实现循环的效果呢?

答案是用递归来模拟,ts没有显式的循环语法,因此我们只能通过递归,当能够通过&分隔提取每一个param的时候就一直递归进行提取,直到提取到最后一个param,已经没有&进行分隔了,就可以退出递归的过程了

用多个简单工具类型组合实现一个复杂工具类型

由于我们的这个工具类型实现起来涉及到的功能比较多,所以合理的做法是需要将其拆分成多个简单的工具类型,每个工具类型完成单一的工作,最后像搭积木一样将它们组合起来即可

那么需要拆分出哪些简单工具类型呢?

  • ParseParam<T>,这个工具类型负责将a=1这样的param解析成{ a: "1" }这样的单一索引类型
  • MergeParams<Param, Rest>,负责将已经解析得到的索引类型合并为一个大的索引类型
  • MergeValues<One, Other>,负责将同一个索引的多个不同值合并成数组返回

通过以上分析后,我们就可以开始做操了!

完成体操代码

/**
 * @description 解析 query params
 *
 * a=1&a=2&b=3&c=4 ==> { a: ["1", "2"], b: "3", c: "4" }
 */

type ParseQueryString<T extends string> =
  // 模式匹配提取 a=1&a=2&b=3&c=4 中的 a=1 和 a=2&b=3&c=4
  T extends `${infer Param}&${infer Rest}`
    ? // 匹配成功则合并他们
      // ParseParam<'a=1'> => { a: "1" }
      // ParseQueryString<'a=2&b=3&c=4'> => { a: "2", b: "3", c: "4" }
      // 合并后 => { a: ["1", "2"], b: "3", c: "4" }
      MergeParams<ParseParam<Param>, ParseQueryString<Rest>>
    : // 匹配失败 说明已经解析到最后一个 param string 了 也就是 c=4
      // 这时候直接单独解析它即可
      ParseParam<T>

// 合并两个已解析的 param 索引类型
// { a: "1" } 和 { a: "2", b: "3", c: "4" } => { a: ["1", "2"], b: "3", c: "4" }
type MergeParams<
  Param extends Record<string, any>,
  Rest extends Record<string, any>,
> = {
  // 遍历 Param 和 Rest 的 key
  // 如果 key 同时存在于 Param 和 Rest 中 则需要调用 MergeValues 进行合并
  // 否则就返回值即可
  [Key in keyof Param | keyof Rest]: Key extends keyof Param
    ? Key extends keyof Rest
      ? MergeValues<Param[Key], Rest[Key]>
      : Param[Key]
    : Key extends keyof Rest
    ? Rest[Key]
    : never
}

// 合并索引类型的值 如果是同一个值就不需要合并 直接返回即可
// 否则就要构造一个数组去保存
type MergeValues<One, Other> = One extends Other
  ? One
  : Other extends unknown[]
  ? [One, ...Other]
  : [One, Other]

// 解析 a=1 为 { a: "1" }
// 要用可索引签名才可以正常添加索引项
type ParseParam<Param extends string> =
  Param extends `${infer Key}=${infer Value}` ? { [K in Key]: Value } : {}

// type Res = {
//   a: ['1', '2']
//   b: '3'
//   c: '4'
// }
type Res = ParseQueryString<'a=1&a=2&b=3&c=4'>