最近在做一个需要对传进来的字符串
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,只不过没有成功,希望如果有大佬成功了,叫一下我。