You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决TypeScript中Conversation与Message序列化类的循环依赖问题

TypeScript序列化类循环依赖问题解决

问题背景

两个DTO序列化类ConversationSerialization和MessageSerialization互相引用,导致编译后的CommonJS模块出现循环依赖错误。使用barrel export仅解决了TypeScript静态检查层面的问题,运行时错误依然存在。

解决方法

1. 动态导入类型(最直接有效)

利用class-transformer和nestjs-swagger支持函数式类型引用的特性,将直接类型替换为动态导入的函数,避免模块初始化时的循环引用:

修改conversation.serialization.ts:

@Exclude()
export class ConversationSerialization {
  @ApiResponseProperty({ type: () => import('./message.serialization').then(m => m.MessageSerialization) })
  @Expose({
    groups: [CONVERSATION.FIND_MY, CONVERSATION.FIND_BY_ID],
  })
  @Type(() => import('./message.serialization').then(m => m.MessageSerialization))
  messages: import('./message.serialization').MessageSerialization[];

  constructor(partial: Partial<ConversationSerialization>) {
    Object.assign(this, partial);
  }
}

修改message.serialization.ts:

@Exclude()
export class MessageSerialization {
  @Type(() => import('./conversation.serialization').then(m => m.ConversationSerialization))
  @ApiResponseProperty({ type: () => import('./conversation.serialization').then(m => m.ConversationSerialization) })
  @Expose({ groups: [] })
  conversation?: import('./conversation.serialization').ConversationSerialization;

  constructor(partial: Partial<MessageSerialization>) {
    Object.assign(this, partial);
  }
}

这种方式让类型解析延迟到真正需要的时候,绕开了模块加载阶段的循环引用。

2. 业务层面切断循环

结合@Expose的groups配置,在不同接口场景下只暴露必要字段,从业务逻辑上避免不必要的互相引用:

  • 获取会话列表时,Message字段只返回基础内容,不携带完整的Conversation引用
  • 获取单条消息时,Conversation字段只返回基础信息,不携带完整的Message列表

如果需要完整的嵌套结构,可以拆分DTO为基础版和扩展版:

  • 创建ConversationBaseSerialization:仅包含会话基础字段,不引用Message
  • 创建MessageBaseSerialization:仅包含消息基础字段,不引用Conversation
  • 在扩展类中根据业务场景引用对方的基础类,避免循环。

3. 切换模块系统

当前使用的CommonJS模块系统对循环引用的处理不够友好,可以切换到ES模块:

  1. 修改tsconfig.json中的module为ESNext,添加moduleResolution: node
  2. 在package.json中添加"type": "module"
    ES模块的循环引用处理机制更灵活,能避免部分运行时错误。

为什么Barrel Export无效?

Barrel文件(如统一导出的index.ts)只是在TypeScript层面做了导出聚合,编译后的CommonJS模块仍然会保留原始的互相引用关系。模块加载时,其中一个模块会先初始化,此时另一个模块还未完成导出,导致拿到undefined,触发运行时错误。


原始问题代码

conversation.serialization.ts

@Exclude()
export class ConversationSerialization {
  @ApiResponseProperty({ type: () => [MessageSerialization] })
  @Expose({
    groups: [CONVERSATION.FIND_MY, CONVERSATION.FIND_BY_ID],
  })
  @Type(() => MessageSerialization)
  messages: MessageSerialization[];

  constructor(partial: Partial<ConversationSerialization>) {
    Object.assign(this, partial);
  }
}

message.serialization.ts

@Exclude()
export class MessageSerialization {
  @Type(() => ConversationSerialization)
  @ApiResponseProperty()
  @Expose({ groups: [] })
  conversation?: ConversationSerialization;

  constructor(partial: Partial<MessageSerialization>) {
    Object.assign(this, partial);
  }
}

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
  }
}

schema.prisma

// 这是你的Prisma schema文件,
// 更多细节参考文档:https://pris.ly/d/prisma-schema

generator client {
  provider     = "prisma-client-js"
  relationMode = "prisma"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id                          String    @id @default(cuid())
  email                       String    @unique @db.VarChar(255)
  name                        String    @db.VarChar(255)
  password                    String    @unique @db.VarChar(255)
  resetPasswordToken          String?   @db.VarChar(255)
  resetPasswordTokenExpiresAt DateTime?

  sentMessages Messages[] @relation("SentMessages")

  seenMessages SeenMessages[]

  sentFriendRequests     FriendsRequest[] @relation("FriendsRequestSender")
  receivedFriendRequests FriendsRequest[] @relation("FriendsRequesReceiver")

  myFriends Friends[] @relation("UserFriends")
  friendsBy Friends[] @relation("UserFriendsBy")

  groupsOwned          Groups[]
  ConversationsOnUsers ConversationsOnUsers[]

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Friends {
  user      User   @relation("UserFriends", fields: [userId], references: [id])
  userId    String
  friend    User   @relation("UserFriendsBy", fields: [friendId], references: [id])
  friendId  String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@id([userId, friendId])
}

model Messages {
  id      String @id @default(cuid())
  message String @db.VarChar(500)

  seenBy SeenMessages[]

  sender   User   @relation(name: "SentMessages", fields: [senderId], references: [id])
  senderId String

  conversation    Conversations @relation(fields: [conversationsId], references: [id])
  conversationsId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([senderId, conversationsId])
}

model SeenMessages {
  seenBy    User   @relation(fields: [seenById], references: [id])
  seenById  String
  message Messages @relation(fields: [messageId], references: [id])
  messageId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@id([seenById, messageId])
}

model Conversations {
  id      String   @id @default(cuid())
  isGroup Boolean? @default(false)

  messages             Messages[]
  group                Groups?
  conversationsOnUsers ConversationsOnUsers[]

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model ConversationsOnUsers {
  user   User   @relation(fields: [userId], references: [id])
  userId String

  conversation    Conversations @relation(fields: [conversationsId], references: [id])
  conversationsId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@id([userId, conversationsId])
}

model FriendsRequest {
  sender     User   @relation(name: "FriendsRequestSender", fields: [senderId], references: [id])
  senderId   String
  receiver   User   @relation(name: "FriendsRequesReceiver", fields: [receiverId], references: [id])
  receiverId String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@id([senderId, receiverId])
}

model Groups {
  name     String   @db.VarChar(255)
  isPublic Boolean? @default(false)

  owner   User   @relation(fields: [ownerId], references: [id])
  ownerId String

  conversation    Conversations @relation(fields: [conversationsId], references: [id])
  conversationsId String        @unique

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@id([ownerId, conversationsId])
}

内容的提问来源于stack exchange,提问作者Eivydas Vickus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 03:05:19