openInula
及其生态组件使用了 TypeScript 语言来进行编写,相比React
使用的flow,
TypeScript 具有更完善灵活的语法、支持函数式编程,另外通过 TypeScript 生成的index.d.ts
也免于开发人员手动维护d.ts
文件。在这个系列中我们将选取多个openInula
TypeScript 的实践来进行介绍。
TypeScript 类型体操实现路由参数自动补全
今天介绍的是使用 TypeScript 类型体操完成路由参数的自动补全,相信大家在阅读inula-router
的源码时会发现有这样的一段代码写在inula-router
的类型声明中:
// 片段1
type ClearLeading<U extends string> = U extends `/${infer R}` ? ClearLeading<R> : U;
type ClearTailing<U extends string> = U extends `${infer L}/` ? ClearTailing<L> : U;
// 片段2
type ParseParam<Param extends string> = Param extends `:${infer R}`
? { [K in R]: string } : {};
// 片段3
type MergeParams<OneParam extends Record<string, any>, OtherParam extends Record<string, any>> = {
readonly [Key in keyof OneParam | keyof OtherParam]?: string;
};
// 片段4
type ParseURLString<Str extends string> = Str extends `${infer Param}/${infer Rest}`
? MergeParams<ParseParam<Param>, ParseURLString<ClearLeading<Rest>>>
: ParseParam<Str>;
// 解析URL中的动态参数,以实现TypeScript提示功能
export type GetURLParams<U extends string> = ParseURLString<ClearLeading<ClearTailing<U>>>;
// Route.tsx
function Route<Path extends string, P extends Record<string, any> = ParseURLString<ClearLeading<ClearTailing<Path>>>>(props: RouteProps<P, Path>)
复制代码
如果不看注释的话,对于不少 Typescript 的使用者来说,或许并不清楚上述的类型声明到底有什么作用。下面让我们来依次讲解每个类型的作用,带你玩转 TypeScript 类型体操。
片段 1:
首先要讲解的是 TypeScript 中的infer
关键词,infer
是 TypeScript4.1 引入的一个关键字,用于声明类型推断变量。它的作用是方便地从一个类型中提取出一个新的类型。在ClearLeading
和ClearTailing
这两个函数中,传入的参数是U
,如果U
是字符串且以/
开头或结尾,就继续递归调用自身,否则就返回U
。
type ClearLeading<U extends string> = U extends `/${infer R}` ? ClearLeading<R> : U;
type ClearTailing<U extends string> = U extends `${infer L}/` ? ClearTailing<L> : U;
复制代码
运行效果:
type Res = ClearLeading<"///hello"> // "Hello"
type Res2 = ClearTailing<"world///"> // "world"
复制代码
片段 2:
看懂了上面的结果,那也就可以很容易地看懂函数ParseParam
了,ParseParam
接受一个字符类型的参数,如果该字符串以:
开头,返回一个键为Param
,值为string
的类型。
type ParseParam<Param extends string> = Param extends `:${infer R}`
? { [K in R]: string } : {};
复制代码
运行效果:
type Res3 = ParseParam<":id"> // {id: string}
type Res4 = ParseParam<"id"> // {}
复制代码
片段 3
这段代码是一个用于合并对象的工具函数,合并后键值的类型均为只读的string
,符合路由中参数的类型。
type MergeParams<OneParam extends Record<string, any>, OtherParam extends Record<string, any>> = {
readonly [Key in keyof OneParam | keyof OtherParam]?: string;
};
复制代码
运行效果
type Res5 = MergeParams<{a:string}, {b:string}> // {readonly a?: string, readonly b?: string}
type Res6 = MergeParams<{a:string,b:string}, {c:number}> // {readonly a?: string, readonly b?: string, readonly c?: string}
复制代码
片段 4:
最后我们来看类型ParseURLString
,该函数接受一个字符串参数,如果该参数中存在/
,则用第一个/
把字符串分为Param
和Rest
两部分,将Param
部分传入ParseParam
函数中,去除Rest
开头的/
后,进行递归调用,并使用工具函数MergeParams
将Param
部分解析出的结果进行合并。递归的出口就是接受一个不包含/
的字符串,返回从该字符串解析出的对象。
type ParseURLString<Str extends string> = Str extends `${infer Param}/${infer Rest}`
? MergeParams<ParseParam<Param>, ParseURLString<ClearLeading<Rest>>>
: ParseParam<Str>;
// 解析URL中的动态参数,以实现TypeScript提示功能
export type GetURLParams<U extends string> = ParseURLString<ClearLeading<ClearTailing<U>>>;
复制代码
运行效果:
type Res7 = GetURLParams<"/home/:name/:id"> // {name?: string, id?: string}
复制代码
自动补全效果图:
通过上面这样的 TypeScript 类型体操再结合泛型的使用就可以实现下图中的效果了,可以在开发者使用时给出更为智能的补全信息:
评论