ts 4.1 模板字符串类型实现一个纯类型层面的url2json

1,046 阅读6分钟

最近在做一个需要对传进来的字符串queryString转化为一个params的方法的时候,发现了一个问题。就是一般情况下,对于这种情况的ts类型,都是使用简单的string还有Record<string, unknown>来进行处理,这种写法不能说不对,只是感觉上应该是可以有更为直观的类型,可以通过key=value这种结构取得{key: value}这样的类型。正好ts4.1更新了模板字符串类型,我想了想,准备试试看看怎么能把这玩意儿弄出来。

开始

首先我们要确定这个类型应该是什么结构的

  • 它需要传递进去一个url,是一个字符串类型
  • 它需要返回一个Record<string, string>类型,分别对应的是我们传递进去的每一个key和value

因此我们可以首先创建一个入口类型

type Url2Json<S extends string> = xxx

具体这个类型应该是什么样呢,我们后面再给他补充

找出共同点

我们给定一个url

const url = 'http://baidu.com?a=1&b=2&c=3'

从js的角度来说,我们需要从?开始进行分割,前部分是进行请求的参数,对于我们来说毫无意义,可以进行忽略,我们只需要关注后面的请求参数部分。

`?a=1&b=2&c=3`

在请求的部分中,我们可以看到一个规律

(\?|&)key=value&?

所有的key都包含在(\?|&)=之间,所有的value都包含在=和大部分&之间(除了最后一个)

开始写代码

我们在写代码之前需要考虑一件事情,就是一个url中,携带的参数可能有多个键值对,所以我们写的类型应该是一个具有递归性质的类型。

而且还有一个需要注意的地方,就是如果是递归性质的类型,那么就需要一个类型来作为每个值的保存者,以此让每个值都可以获取,进行最后的拼接。

type GetQueryString<S extends string> = S extends `${string}?${infer R}` ? doSomeThing : never

在这儿我定义了一个GetQueryString的类型,用于判断是否存在query参数,如果存在,那么就让它继续做某些事情,如果不存在,则不进行后续操作。

然后我们开始写获取了query之后的操作

按最简单的来说,我们获取到的query应该是一个?key=val的结构,根据这个结构,我们可以接着往下写这个doSomeThing

type GetQueryKeys<S extends string, D = ''> = S extends `${infer T}=${any}`
  ? doSomeThing
  : D

我们在这儿定义了两个泛型。第一个S就是我们要进行类型转换的url字符串里的query,也就是上面的GetQueryStringR,它大概是这么个结构。

`a=1&b=2&c=3`

而第二个,就是我们用来保存所有key的一个泛型,在初始化的时候我们给定它为一个空字符串。

首先,我们需要判断这个query,是否是合格的键值对结构,所以我们使用S extends来进行判断,如果它是一个合格的query结构,那么再对它进行其他操作,如果不是,则直接返回D

在满足query结构之后,我们再来对它进行拆分,也就是继续编写上面的doSomeThing

S extends `${string}&${infer P}`
	? GetQueryKeys<P, D | T>
	: D | T

在上面已经确定了是query结构的基础上,我们能够获取key,但是我们还要判断当前这个query,是否是多个键值对组成,因为多个键值对的情况下,会存在&,我们需要对其进行处理

我们使用extends &来判断是否存在多个键值对,并用infer来声明后续query类型。

  • 如果存在,那么对其进行递归求值
  • 否则,返回当前key ->T以及历史key -> D组成的联合类型

所以,这个用来获取所有key的类型,应该是这样

type GetQueryKeys<S extends string, D = ''> = S extends `${infer T}=${any}`
  ? S extends `${string}&${infer P}`
    ? GetQueryKeys<P, D | T>
    : D | T
  : D

到这一步,我们可以首先来看看效果了

image.png

image.png

和我们想的一样,但是就是多了个空字符串类型,怎么办呢?不用着急,我们后面会处理它的。

根据key从url取得value

在上面,我们已经取得了所有的key,但是我们最终需要的是一个对象结构的类型,因此我们需要把key组成的联合类型转换为键值对的形式。

type QueryParams<S extends string> = Record<GetQueryString<S>, unknown>

我们可以使用高阶类型Record来进行类型的转换

image.png

但是,现在就会比较突兀的看到这个空字符串了,不用担心,我们可以使用高阶类型Omit来排除它

image.png

到现在为止,这个类型的雏形就已经有了,剩下的,就是我们按照key来去填充value的类型了。

在声明获取value的类型之前,我们也需要确定几件事

  • 这个类型中需要从url中取得对应的value,所以我们需要一个url的泛型
  • 这个类型是基于上面的Omit之后的类型进行替换的,所以也需要一个泛型来传递这个query对象
  • 要考虑到?&

根据这几点,我们开始着手编写类型

type GetValue<Params, URL extends string> = {
  [P in keyof Params & string]: URL extends `${any}${'?' | '&'}${P}=${infer R}`
    ? R extends `${infer K}&${any}`
      ? K
      : R
    : Params[P]
}

这个类型我就不往开拆解了,能看到这儿的相信都可以读懂它,我就大概说一下

如果这个url是存在(?|&)当前key=xxx的时候,我们通过infer来获取等号后面的值R,并且对这个R进行判断,是否存在后续&,如果存在,那么通过infer获取&之前的K,也就是=&之间的那个值,否则说明R就是最后一个类型,直接返回R。如果不存在的话,那么直接返回Params[P],也就是我们之前定义好的unknown

至此,我们从一个字符串中,获取到了它的key组成的联合类型,并用其组合成一个对象结构的类型,最后根据key在url中获取value,进行类型拼接。

代码

type GetQueryKeys<S extends string, D = ''> = S extends `${infer T}=${any}`
  ? S extends `${string}&${infer P}`
    ? GetQueryKeys<P, D | T>
    : D | T
  : D
type GetQueryString<S extends string> = S extends `${string}?${infer R}` ? GetQueryKeys<R> : never
type GetValue<Params, URL extends string> = {
  [P in keyof Params & string]: URL extends `${any}${'?' | '&'}${P}=${infer R}`
    ? R extends `${infer K}&${any}`
      ? K
      : R
    : Params[P]
}
type QueryParams<S extends string> = Record<GetQueryString<S>, unknown>
type Url2Json<S extends string> = Omit<GetValue<QueryParams<S>, S> ,''>

效果

image.png

结语

ts4.1的这个模板字符串类型其实挺有意思的,我之前看过一个大佬写的能够通过这个获取vuexdispatch数据,觉得惊为天人,因为我确实没想道这玩意儿还能这么用。后面想试试也没啥思路试它,然后正好遇到这么个需求,瞬间有了想法。

这个算是纯类型版的url2json,其实我还试着反过来做一个json2url,只不过没有成功,希望如果有大佬成功了,叫一下我。