解决Nestjs中使用swagger但是返回的类型是存在继承和泛型导致的循环依赖项错误
前言
最近在Nestjs项目中使用了swagger来帮助生成api文档,但是当我在返回的类上使用了泛型来传递类型约束的时候,swagger就报错了。
我先声明了一个父类用于复用分页属性:
// pagination.entity.ts
export interface PaginationEntityData<T> {
current_page: number;
page_size: number;
total: number;
list: T[];
}
export class PaginationEntity<T> {
/** 当前页 */
current_page: number;
/** 分页大小 */
page_size: number;
/** 总数量 */
total: number;
/** 列表 */
list: T[];
constructor(data: PaginationEntityData<T>) {
Object.assign(this, data);
}
}
想法很简单,就是不想每次写分页接口时都要重复写这几个属性,然后也不能每次分页接口返回的对象属性都不一样,所以整一个标准父类,通过继承父类来实现标准化。
子类我就这么实现:
// test.entity.ts
import type { PaginationEntityData } from "@common/entities/pagination.entity";
import { PaginationEntity } from "@common/entities/pagination.entity";
import { User } from "@prisma/client";
class ListItem implements User {
name:string;
}
interface TestListEntityData extends PaginationEntityData<User> {}
export class TestListEntity extends PaginationEntity<ListItem> {
constructor(data: TestListEntityData) {
super({
current_page: data.current_page,
page_size: data.page_size,
total: data.total,
list: data.list.map((item) => new ListItem(item))
});
}
}
声明一个TestListEntityData
参数类型,要求外部传递过来的参数,他的list数组里面的值必须是User
类型,User类型是用prisma获取的用户模型数据类型。
ListItem
是最终返回给客户端的数组item对象,我们可以在这个对象上做一些操作,比如使用class-transformer
插件的Exclude
来屏蔽某些属性,最终通过全局的序列化拦截器ClassSerializerInterceptor
来去除Exclude标记的属性。
这里就不过多展示了,有兴趣可以查看我之前的一篇序列化响应的文章,自己在本博客搜索吧。
在子类TestListEntity
的构造函数中,我们接收到参数,并通过super
将数据都传递给父级,子类其实就做了一个最重要的操作,就是list的具体属性是在子类才能确定的,所以通过map生成了ListItem的数组。
父类通过泛型进行约束,从而不会类型报错。
到这里一切都没问题,直到我给控制器声明api返回的数据类型是TestListEntity
的时候,就发生了报错。
@ApiResponse({ status: HttpStatus.OK, type: TestListEntity })
报错如下:
throw new Error(`A circular dependency has been detected (property key: "${key}"). Please, make sure that each side of a bidirectional relationships are using lazy resolvers ("type: () => ClassType").`);
^
Error: A circular dependency has been detected (property key: "list"). Please, make sure that each side of a bidirectional relationships are using lazy resolvers ("type: () => ClassType").
at SchemaObjectFactory.createNotBuiltInTypeReference
这里我把错误精简了一下,主要就是A circular dependency has been detected 这报错,检测到循环依赖项,但是实际上并没有在写法上存在循环引入,不然项目都启不起来。
分析错误
错误提示的是循环依赖类型,事实上这个循环不是我们所谓的import引入循环,我觉得是swagger自己在类型获取上导致的循环。
因为我们是通过泛型来传递ListItem
类型,这个类型在TestListEntity上用了,在父类PaginationEntity上也用到了,我怀疑就是这里产生的问题,它可能两边都拿到了同一个类,导致报错了。
为此我还询问了下AI:
根据您提供的错误信息,“A circular dependency has been detected (property key: “list”)”,这表明在 Swagger 文档生成过程中出现了循环依赖的问题。具体来说,它在处理 list 属性时检测到了一种循环引用关系。
原因:
当你在 TestListEntity 中使用 PaginationEntity
并在 PaginationEntity 中定义 list: T[],Swagger 系统会试图解析这个 list 属性。它尝试去推测这个属性的类型,结果因为涉及到的类型(在这里是 ListItem)之间相互引用,导致出现了循环依赖。
大概就是这么一回事,那么该如何解决呢?
解决方案
方案一
我们给子类明确声明一个list属性和对应的类型。
export class TestListEntity extends PaginationEntity<ListItem> {
list: ListItem[] = [];
constructor(data: TestListEntityData) {
super({
current_page: data.current_page,
page_size: data.page_size,
total: data.total,
list: []
});
this.list = data.list.map((item) => new ListItem(item));
}
}
当我们通过子类的属性来覆盖父类属性时,必须要有初始值,所以这里设置了一个空数组。
然后给父级传递参数list就传个空,因为最终我们使用的是子类的list。
但是这种方式它有一个很蛋疼的问题,就是它改写了数据结构了,父类的list约束没有起到任何作用,反而还因为它导致写法都发生了改变,虽然这一切并不是代码的问题,是swagger导致的。
方案二
这种方式我都觉得很神奇,因为它使用了一个平时很少用到的修饰符declare
,我敢说这个修饰符除非在写全局类型声明的时候有用到,其他地方能用到都少之又少了。
export class TestListEntity extends PaginationEntity<ListItem> {
declare list: ListItem[];
constructor(data: TestListEntityData) {
super({
current_page: data.current_page,
page_size: data.page_size,
total: data.total,
list: data.list.map((item) => new ListItem(item))
});
}
}
这种方式我们只需要在子类声明一下list属性类型就行了,也不用默认赋值,父类super也不需要任何改动,已经堪称完美了。
对我而言,这个写法简直魔法一般。
问了下AI解释如下:
在 TypeScript 中,declare 关键字用于告诉编译器某些变量、属性或方法的类型或存在性,而不需要实际的实现。这对于与现有的 JavaScript 代码进行互操作或告诉编译器一些外部定义的内容非常有用。
在你的代码中,declare list: ListItem[]; 看起来很像是你在告诉 TypeScript 编译器这个类拥有一个 list 属性,并且这个属性的类型是 ListItem[]。但是,由于你的父类 PaginationEntity 似乎也有一个 list 属性,所以这里使用 declare 有几个潜在的含义:
- 避免重复声明:你在子类中避免了对 list 属性的重复声明。即,你不再需要重新初始化或覆盖它,只需声明它的类型。
- 类型强化:即使父类已经拥有一个 list 属性,你也许希望在子类中明确地定义 list 的类型。这可以提高代码的可读性和可维护性。
- 编译器提示:使用 declare 可以让编译器在类型检查时更清楚地知道这个属性的类型。它有助于避免一些潜在的类型错误。
- 整体效果是你明确告诉 TypeScript 编译器这个属性存在于子类中,并且其类型为 ListItem[],尽管它已经存在于父类中。
最终这种方式也能避免swagger的循环类型报错。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据