前言

最近在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的循环类型报错。

分类: Nest.js 标签: declareNestjsswagger

评论

暂无评论数据

暂无评论数据

目录