最近在做一个需要对传进来的字符串
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,也就是上面的GetQueryString
的R
,它大概是这么个结构。
`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
复制代码
到这一步,我们可以首先来看看效果了
和我们想的一样,但是就是多了个空字符串类型,怎么办呢?不用着急,我们后面会处理它的。
根据key从url取得value
在上面,我们已经取得了所有的key,但是我们最终需要的是一个对象结构的类型,因此我们需要把key组成的联合类型转换为键值对的形式。
type QueryParams<S extends string> = Record<GetQueryString<S>, unknown>
复制代码
我们可以使用高阶类型Record
来进行类型的转换
但是,现在就会比较突兀的看到这个空字符串了,不用担心,我们可以使用高阶类型Omit
来排除它
到现在为止,这个类型的雏形就已经有了,剩下的,就是我们按照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> ,''>
复制代码
效果
结语
ts4.1的这个模板字符串类型其实挺有意思的,我之前看过一个大佬写的能够通过这个获取vuex
的dispatch
数据,觉得惊为天人,因为我确实没想道这玩意儿还能这么用。后面想试试也没啥思路试它,然后正好遇到这么个需求,瞬间有了想法。
这个算是纯类型版的url2json
,其实我还试着反过来做一个json2url
,只不过没有成功,希望如果有大佬成功了,叫一下我。