不知道在你的日常工作中,是否出现过这样的场景:明明 Typescript
官方文档已经看了很多遍,实际写起代码来却各种煎熬,遇到报错,在搜索无果之后,无奈写下 any。?♀️
(我猜有,不然你也不会点开这篇文章。?
而阻碍你强类型更近一步的,绝大多数情况下是因为泛型还没完全掌握。这篇文章将从我日常工作中遇到的一个例子入手,一步步介绍哪里需要用到泛型,怎么写~
(如果除了泛型,Typescript 其他知识点也不太熟怎么办 ? ?可以我之前整理的另一篇比较全面的文章结合实例学习 Typescript。
Let's begin。
问题
说,后端提供了多个支持分页查列表数据的接口,这些接口的参数格式、响应结果、分页形式可能都不一样。拿分页形式来说,常见的分页参数类型就有好几种,传页数和每页数量、传偏移值和 limit、使用上一页最后一个 id 来查询等等。
{
page_size: number,
page_num: number
}
{
offset: number,
limit: number
}
{
forward: boolean
last_id: string
page_size: number
}
...
这些接口数据量都在几千条数据左右,考虑数据库的压力,后端同学不建议一次拉几千条数据,需要前端分页去全部拉取。
为了避免分页的逻辑每个接口都写一次,要求实现一个强类型的工具方法,实现自动分页拉取全部数据的功能。
代码实现
这篇文章的重点不在如何实现这样的功能,简单画一下流程图,相信大部分人都能实现。
一份可行的代码实现如下:
const unpaginate = (
api,
config,
) => {
const { getParams, hasMore, dataAdaptor } = config
async function iterator(time, lastRes) {
// 通过上一次请求结果和第几次请求获取下一次请求的参数
const params = getParams(lastRes, time)
const res = await api(params)
let next = []
// 如果还有下一页,继续拉取
if (hasMore(res, params)) {
next = await iterator(time + 1, res)
}
// 拼接结果一起返回
return dataAdaptor(res).concat(next)
}
return iterator()
}
代码解读:unpaginate
方法第一个参数传入一个返回 Promise 结果的 api 方法;第二个参数支持传入一个可配置对象:
getParams
方法会把上一次请求的结果以及当前是第几次请求回传,方便使用者设置请求参数;
hasMore
方法会回传当前请求的结果和参数,需要使用者告知程序是否已经拉取完毕;
dataAdaptor
方法则把每次请求得到的结果,回传回去允许自定义返回结果的格式(例如把某个字段下划线改成驼峰),并把返回值作为最终结果存下来;
想一想,你在用 Typescript
的时是否也实现过类型的功能,类型安全吗?编码时会有代码提示吗?还是说也是 any
一把梭呢?
接下来,我们将为一步一步为这个方法提供类型支持。
Typescritp 泛型加持
首先从参数入手,为 api 和 config 编写最基本的类型声明。
export interface Config {
hasMore: (res?: any, params?: any) => boolean
getParams: (res?: any, time?: number) => any
dataAdaptor: (res: any) => any[]
}
const unpaginate = (
api: (params: any) => Promise<any[]>,
config: Config,
): Promise<any[]> => {
...
}
上面的类型声明能起的作用不大(因为到处是 any
),不过也比没有好,至少在给 api
和 config
传不符合类型的参数时会报错。
第一个泛型——参数类型
很容易看到,Config
类型中方法的参数和 api
类型强关联。api
的参数的类型决定了 hasMore
方法的 params
参数类型。而返回结果的类型,三个方法都会用到了。
type EventListenerParamsType = Parameters<typeof window.addEventListener>;
// [type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined]
type A = (a: number) => string
type B = ReturnType<A>
// string
而这里 api
不是固定的类型,需要根据动态的 api
类型上提取类型,泛型登场。
const unpaginate = <T extends (params: any) => Promise<any>>(
api: T,
config: Config,
): Promise<any[]> => {
...
}
我们在方法前加上了 <T extends (params: any) => Promise<any>>
这段代码,表示声明了一个泛型,extends
限制了这个泛型的下限:必须是一个方法,并且返回一个 Promise 结果。
然后又将 T
类型赋予 api
,这样写完后面再使用类型 T
,Typescript 就动态地根据实际调用的 api
方法类型自动推导了。
api
是泛型,Config
当然也需要是泛型,泛型是当做参数可以传递的。
export interface Config<P> {
hasMore: (res?: R, params?: P) => boolean
// ...
}
interface Config<P>
这里我们让 Config 也支持了泛型参数,将其传给了 parmas
参数。可以认为这里的 P
只是随意起的变量名,换成 T
也是可以的。
结合 Parameters
泛型工具方法,取 T
的第一个参数类型传给 Config
,这样它们的类型就关联起来了。
const unpaginate = <T extends (params: any) => Promise<any>>(
api: T,
config: Config<Parameters<T>[0]>,
): Promise<any[]> => {
...
}
Parameters<T>[0]
的意思是,取 T 类型的参数(是一个数组类型)的第一个参数类型。
第二个泛型——返回值的类型
参数类型能动态推导出来,按道理 api
的返回结果也可以使用同样的操作实现。
不过这里会遇到一个棘手的问题,api
返回结果的类型是 Promsie<R>
,而 config 回传回去的结果应该去 Promise
化的 R
类型。
从泛型中提取类型,我们会用到 infer
,直接看代码吧:
type UnPromise<T> = T extends Promise<infer U> ? U : undefined
type A = Promise<number>
type B = UnPromise<A>
// number
如果说泛型是动态类型,infer 就是动态的动态类型。上面的例子中,我们在 extends
子句中使用,告诉 Typescript
这里的类型需要动态推导一下。
提取出了返回值的实体类型,继续完善类型定义:
export interface Config<P, R> {
hasMore: (res?: R, params?: P) => boolean
getParams: (res?: R, time?: number) => Partial<P>
dataAdaptor: (res: R) => any[]
}
type UnPromise<T> = T extends Promise<infer U> ? U : undefined
const unpaginate = <
T extends (params: any) => Promise<any>,
U extends UnPromise<ReturnType<T>>
>(
api: T,
config: Config<Parameters<T>[0], U>,
): Promise<any[]> => {
...
}
第二个泛型 U
是动态从 UnPromise<ReturnType<T>>
推导出来的,然后再将其传递给 Config
就完成了返回结果的类型传导。
第三个泛型——格式化后的结果类型
剩下最后一个要处理的问题,是 dataAdaptor
的返回值结果类型。我们对其返回结果没有任何限制,需要做的也是让 Typescirpt 自行推导和传递。
并做为 unpaginate
方法的返回结果类型。
这里需要再定义一个泛型:
export interface Config<P, R, V> {
// ...
dataAdaptor: (res: R) => V[]
}
const unpaginate = <
T extends (params: any) => Promise<any>,
U extends UnPromise<ReturnType<T>>,
V extends any
>(
api: T,
config: Config<Parameters<T>[0], U, V>,
): Promise<V[]>
我们使用 V extends any
定义了新的泛型类型,将其传递给 Config.dataAdaptor
的返回结果,dataAdaptor: (res: R) => V[]
这样 Typescript 在具体的场景下就可以根据 dataAdaptor
返回的数组类型 => 推导出 V
的类型了。
再将 V[]
作为 unpaginate
的返回值类型,这样就可以全串起来了。
最终效果
API 方法参数推导:
API 方法返回结果推导:
格式化后返回结果推导:
可以在Typescript playground 上体验,代码也可以在我的 github 上找到。
Ending
这篇文章通过一步步介绍如何使用泛型为一个通用方法实现类型声明,希望看完之后对你有所帮助。对 Typescript 还不太熟悉的同学可以看我之前写的另一篇文章《结合实例学习 Typescript》
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!