深圳幻海软件技术有限公司 欢迎您!

TS 类型体操:图解一个复杂高级类型

2023-02-28

之前我们零散地了解了一些TypeScript类型体操的套路,但是没有综合练习下,今天就来做个高难度的体操,它会综合运用模式匹配、构造、递归等套路,对提升类型编程水平很有帮助。我们要实现的高级类型如下:它的类型参数是参数字符串querystring,会返回解析出的参数对象,如果有同名的参数,会把值做合

之前我们零散地了解了一些 TypeScript 类型体操的套路,但是没有综合练习下,今天就来做个高难度的体操,它会综合运用模式匹配、构造、递归等套路,对提升类型编程水平很有帮助。

我们要实现的高级类型如下:

它的类型参数是参数字符串 query string,会返回解析出的参数对象,如果有同名的参数,会把值做合并。

先不着急实现,我们先回顾下相关的类型体操基础:

类型体操基础

模式匹配

模式匹配是指用一个类型匹配一个模式类型来提取其中的部分类型到 infer 声明的局部变量中。

比如提取 a=b 中的 a 和 b:

这种模式匹配的套路在数组、字符串、函数等类型中都有很多应用。

详细了解可以看之前的一篇文章:模式匹配-让你 ts 类型体操水平暴增的套路

构造

映射类型用于生成索引类型,生成的过程中可以对索引或者索引值做一些修改。

比如指定 key 和 value 来生成一个索引类型:

详细了解可以看之前的一篇文章:TS 类型体操:索引类型的映射再映射

递归

TypeScript 高级类型支持递归,可以处理数量不确定的问题。

比如不确定长度的字符串的反转:

type ReverseStr< 
    Str extends string,
    Result extends string = '' 
> = Str extends `${infer First}${infer Rest}` 
    ? ReverseStr<Rest, `${First}${Result}`> 
    : Result;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

简单了解下模式匹配、构造、递归都是什么之后,就可以开始实现这个复杂的高级类型 ParseQueryString 了:

思路分析

假设有这样一个 query string:a=1&a=2&b=3&c=4。

我们要首先把它分成 4 部分:也就是 a=1、a=2、b=3、c=4。这个就是用通过上面讲的模式匹配来提取。

每一部分又可以进一步处理,提取出 key value 构造成索引类型,比如 a=1 就可以通过模式匹配提取出 a、1,然后构造成索引类型 {a: 1}。

这样就有了 4 个索引类型 {a:1}、{a:2}、{b:3}、{c:4}。

结下来把它合并成一个就可以了,合并的时候如果有相同的 key 的值,要放到数组里。

就产生了最终的索引类型:{a: [1,2], b: 3, c: 4}

整体流程是这样的:

其中第一步并不知道有多少个 a=1、b=2 这种 query param,所以要递归的做模式匹配来提取。

这就是这个高级类型的实现思路。

下面我们具体来写一下:

代码实现

我们按照上图的顺序来实现,首先提取 query string 中的每一个 query param:

query param 数量不确定,所以要用递归:

type ParseQueryString<Str extends string>
    = Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>> 
        : ParseParam<Str>;
  • 1.
  • 2.
  • 3.
  • 4.

类型参数 Str 为待处理的 query string。

通过模式匹配提取其中第一个 query param 到 infer 声明的局部变量 Param 中,剩余的字符串放到 Rest 中。

用 ParseParam 来处理 Param,剩余的递归处理,最后把它们合并到一起,也就是 MergeParams 。

如果模式匹配不满足,说明还剩下最后一个 query param 了,也用 ParseParam 处理。

然后分别实现每一个 query param 的 parse:

这个就是用模式匹配提取 key 和 value,然后构造一个索引类型:

type ParseParam<Param extends string> 
    = Param extends `${infer Key}=${infer Value}` 
        ? { [K in Key]: Value } 
        : {};
  • 1.
  • 2.
  • 3.
  • 4.

这里构造索引类型用的就是映射类型的语法。

先来测试下这个 ParseParam:

做完每一个 query param 的解析了,之后把它们合并到一起就行:

合并的部分就是 MergeParams:

type MergeParams<
    OneParam extends object,
    OtherParam extends object
> = {
  [Key in keyof OneParam | keyof OtherParam]: 
    Key extends keyof OneParam
        ? Key extends keyof OtherParam
            ? MergeValues<OneParam[Key], OtherParam[Key]>
            : OneParam[Key]
        : Key extends keyof OtherParam 
            ? OtherParam[Key] 
            : never
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

两个索引类型的合并也是要用映射类型的语法构造一个新的索引类型。

key 是取自两者也就是 key in keyof OneParam | keyof OtherParam。

value 要分两种情况:

  • 如果两个索引类型都有的 key,就要做合并,也就是 MergeValues。
  • 如果只有其中一个索引类型有,那就取它的值,也就是 OtherParam[key] 或者 OneParam[Key]。

合并的时候,如果两者一样就返回任意一个,如果不一样,就合并到数组里返回,也就是 [One, Other]。如果本来是数组的话,那就是数组的合并 [One, ...Other]。

type MergeValues<One, Other> = 
    One extends Other 
        ? One
        : Other extends unknown[]
            ? [One, ...Other]
            : [One, Other];
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

测试下 MergeValues:

这样,我们就实现了整个高级类型,整体测试下:

这个案例综合运用到了递归、模式提取、构造的套路,还是比较复杂的。

可以对照着这张图来看下完整代码:

type ParseParam<Param extends string> = 
    Param extends `${infer Key}=${infer Value}`
        ? {
            [K in Key]: Value 
        } : {};

type MergeValues<One, Other> = 
    One extends Other 
        ? One
        : Other extends unknown[]
            ? [One, ...Other]
            : [One, Other];

type MergeParams<
    OneParam extends object,
    OtherParam extends object
> = {
  [Key in keyof OneParam | keyof OtherParam]: 
    Key extends keyof OneParam
        ? Key extends keyof OtherParam
            ? MergeValues<OneParam[Key], OtherParam[Key]>
            : OneParam[Key]
        : Key extends keyof OtherParam 
            ? OtherParam[Key] 
            : never
}

type ParseQueryString<Str extends string> = 
    Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>>
        : ParseParam<Str>;


type ParseQueryStringResult = ParseQueryString<'a=1&a=2&b=2&c=3'>;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

总结

我们首先复习了下 3 种类型体操的套路:

模式匹配:一个类型匹配一个模式类型,提取其中的部分类型到 infer 声明的局部变量中

构造:通过映射类型的语法来构造新的索引类型,构造过程中可以对索引和值做一些修改

递归:当处理数量不确定的类型时,可以每次只处理一个,剩下的递归来做

然后用这些套路来实现了一个 ParseQueryString 的复杂高级类型。

如果能独立实现这个高级类型,说明你对这三种类型体操的套路掌握的就挺不错的了。