☰
TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂实现可复用的 `PaginatedResponse` 等模式
2026/9/28 6:08:09 网站建设 项目流程
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载

导读

在 GraphQL API 开发中,分页、连接(Connection)等场景往往需要一组"字段结构固定、但元素类型可变"的返回类型,例如items: T[]。TypeScript 的装饰器反射能力无法直接支持带类型参数的泛型类与@ObjectType等装饰器组合,TypeGraphQL 因此提供了一套基于"类工厂(class factory)"的泛型类型方案。本文将基于 generic-types.md(本文同时对应 version-2.0.0-beta.4 版本文档),从基础用法、复杂泛型值、免继承的类型工厂三种写法逐步展开,并深入仓库源码与测试,帮助你完整掌握这一模式并直接应用到自己的项目中。

为什么需要泛型类型

类型继承 是减少代码重复的常用手段——把公共字段提取到基类,子类继承即可。但继承只能解决"固定字段集合"的复用问题;当字段的类型本身需要随使用场景变化时(例如分页响应的items元素类型),继承就无能为力了:

@ObjectType() class PaginatedUserResponse { @Field(type => [User]) items: User[]; @Field(type => Int) total: number; @Field() hasMore: boolean; } @ObjectType() class PaginatedRecipeResponse { @Field(type => [Recipe]) items: Recipe[]; // total / hasMore 字段重复了 @Field(type => Int) total: number; @Field() hasMore: boolean; }

如果为每个实体类型都手写一遍分页包装类型,total、hasMore等样板字段会大量重复。这正是 TypeGraphQL 引入泛型类型支持的动机:允许把字段类型当作"参数"传递,从而一套模板复用于任意元素类型。

核心限制:为什么不能直接用泛型类

TypeGraphQL 依赖reflect-metadata在运行时读取design:type元数据来确定字段类型。查看 findType.ts 的实现可知,字段类型主要来自两条路径:

  • 装饰器显式传入的returnTypeFunc(如@Field(type => [TItemClass]));
  • 反射得到的design:type元数据(即Reflect.getMetadata("design:type", prototype, propertyKey)的结果)。

而 TypeScript 的"有限反射能力"意味着:类型参数(type parameter)在编译后的运行时并不存在。一个形如class PaginatedResponse<T> { items: T[] }的标准泛型类,其items字段的design:type只会是Object,装饰器拿不到真实的元素类型;同时装饰器求值发生在类定义时,此时类型参数尚未实例化,也无法与具体类绑定。因此官方文档明确指出:装饰器无法与标准泛型类直接组合,只能借用"类创建器(class-creator)"模式——即与 Resolver 继承 中createBaseResolver工厂函数相同的思路。

基础用法:abstract 类工厂 + 继承

第一步:定义类工厂函数

先把返回类型封装成一个函数,函数内部创建并返回一个abstract类:

export default function PaginatedResponse() { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }

第二步:让工厂接收类型参数对应的运行时值

要获得"泛型"行为,工厂函数本身必须是泛型函数,并接收一个与类型参数相关的运行时参数(也就是实际要作为字段类型的类):

export default function PaginatedResponse<TItem extends object>(TItemClass: ClassType<TItem>) { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }

这里的ClassType<TItem>是 TypeGraphQL 提供的工具类型,其定义位于 ClassType.ts:

export type ClassType<T extends object = object, Arguments extends unknown[] = any[]> = Constructor< T, Arguments > & { prototype: T; };

它表示"能以T为实例类型被构造的类",既描述了运行时能拿到的类对象,也约束了类型参数的形状。

第三步:给内部类添加装饰器

工厂返回的类可以像普通类一样使用@ObjectType、@InterfaceType或@InputType装饰器,取决于你要把它用作输出对象类型、接口还是输入类型:

export default function PaginatedResponse<TItem extends object>(TItemClass: ClassType<TItem>) { @ObjectType() abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }

第四步:像普通类一样声明字段

字段声明与常规类型完全一致,唯一区别是:类型参数TItem与运行时参数TItemClass配对使用——@Field的返回类型函数引用运行时值,属性类型使用类型参数:

export default function PaginatedResponse<TItem extends object>(TItemClass: ClassType<TItem>) { @ObjectType() abstract class PaginatedResponseClass { // 运行时参数:告诉 TypeGraphQL 元素类型是什么 @Field(type => [TItemClass]) // 泛型类型:让 TypeScript 保持类型安全 items: TItem[]; @Field(type => Int) total: number; @Field() hasMore: boolean; } return PaginatedResponseClass; }

第五步:实例化"泛型类型"

使用工厂函数为具体实体创建专属类型类,并可在子类中新增字段或覆盖已有字段的类型:

@ObjectType() class PaginatedUserResponse extends PaginatedResponse(User) { // 可以新增字段 @Field(type => [String]) otherInfo: string[]; }

abstract关键字在这里至关重要:抽象类工厂生成的类型不会作为独立类型注册进 schema(关于这一点的验证见下文"测试验证"部分),因此它只充当"模板",必须通过继承产生具体类型才能被 schema 使用。

第六步:在 Resolver 中使用

@Resolver() class UserResolver { @Query() users(): PaginatedUserResponse { // 自定义业务逻辑,取决于底层数据源与库 return { items, total, hasMore, otherInfo, }; } }

复杂泛型类型值:不止是类

前面的例子中,工厂参数必须是"类"。但@Field装饰器实际接受的值范围更广——包括GraphQLScalarType、String、Number、Boolean等。泛型工厂的参数类型本质上是"可以传给@Field的值"(参见 Field 装饰器与 findType 的类型解析逻辑),所以如果需要items是字符串数组等标量列表,就要放宽函数签名:

export default function PaginatedResponse<TItemsFieldValue extends object>( itemsFieldValue: ClassType<TItemsFieldValue> | GraphQLScalarType | String | Number | Boolean, ) { @ObjectType() abstract class PaginatedResponseClass { @Field(type => [itemsFieldValue]) items: TItemsFieldValue[]; // ... 其他字段 } return PaginatedResponseClass; }

使用时传入对应的运行时值:

@ObjectType() class PaginatedStringsResponse extends PaginatedResponse<string>(String) { // ... }

注意类型参数与运行时值必须匹配:PaginatedResponse<string>(String)中,泛型参数string描述属性类型,运行时值String告诉 schema 生成器字段的 GraphQL 类型。

类型工厂:免继承的直接注册方式

前文方案依赖abstract类 + 继承。TypeGraphQL 还提供另一种方式:不写abstract,让工厂直接生成可注册的类型类。但有两点必须注意:

  1. 类型会被注册进 schema:非抽象类工厂产生的类型会作为独立类型出现在 schema 中,因此不推荐用这种方式再扩展字段;
  2. 必须提供唯一的类型名:工厂每次调用都会生成同名类(如PaginatedResponseClass),如果同一个工厂被多次实例化,schema 中会出现重复的类型名并导致构建错误。解决办法是用@ObjectType的第一个参数动态生成类型名:
export default function PaginatedResponse<TItem extends object>(TItemClass: ClassType<TItem>) { // 提供在 schema 中使用的唯一类型名 @ObjectType(`Paginated${TItemClass.name}Response`) class PaginatedResponseClass { // ... } return PaginatedResponseClass; }

这样PaginatedResponse(User)会生成名为PaginatedUserResponse的 GraphQL 类型,PaginatedResponse(Recipe)则生成PaginatedRecipeResponse,互不冲突。@ObjectType的第一个参数即类型名,其签名可见于 ObjectType.ts(ObjectType(name: string, options?))。

随后把生成的类存进变量,并同时创建同名的类型别名,才能在 Resolver 中既当运行时对象又当类型使用:

const PaginatedUserResponse = PaginatedResponse(User); type PaginatedUserResponse = InstanceType<typeof PaginatedUserResponse>; @Resolver() class UserResolver { // 给装饰器提供运行时类型参数 @Query(returns => PaginatedUserResponse) users(): PaginatedUserResponse { // 实现同前 } }
  • const PaginatedUserResponse:运行时值,用于@Query(returns => ...)装饰器;
  • type PaginatedUserResponse = InstanceType<typeof PaginatedUserResponse>:类型别名,用于方法返回类型标注。

仓库示例与运行验证

仓库 examples/generic-types 提供了完整的可运行示例,与文档中的抽象工厂方案一致:

  • paginated-response.type.ts:定义PaginatedResponse工厂,内部为@ObjectType()抽象的PaginatedResponseClass,字段为items(列表)、total(Int)、hasMore(Boolean)。参数类型为ClassType<TItemsFieldValue> | string | number | boolean,对应文档中"复杂泛型值"的思路;
  • recipe.type.ts:实体Recipe类型;
  • recipe.resolver.ts:创建@ObjectType() class RecipesResponse extends PaginatedResponse(Recipe),并在@Query中返回分页结果,同时提供一个addSampleRecipeMutation;
  • recipe.data.ts:内存示例数据;
  • index.ts:用buildSchema({ resolvers: [RecipeResolver], emitSchemaFile: ... })构建 schema 并启动 Apollo Server(默认监听 4000 端口)。

运行示例后,schema.graphql 中会生成如下类型,直观展示泛型效果:

type Query { recipes(first: Int = 10): RecipesResponse! } type RecipesResponse { hasMore: Boolean! items: [Recipe!]! total: Int! }

items被解析为[Recipe!]!,而total、hasMore来自共享模板——这正是泛型类型要解决的问题。

测试验证:源码如何保证该模式正确

仓库 generic-types.ts 测试 从多个维度验证了这套模式的底层行为,可作为理解原理的实证:

  1. 抽象类不会注册进 schema:测试"shouldn't emit unused abstract object type"(L33-L66)证明,只有@ObjectType() abstract class BaseType被继承、但自身未被引用时,introspection 结果中不存在BaseType,只有SampleType。这是抽象类工厂能安全用作模板的前提。

  2. 多子类共享同一工厂:测试"multiple children of base generic class"(L148-L257)用非抽象工厂Connection<TItem>+ 动态类型名@ObjectType(\${TItemClass.name}Connection`),同时生成UserConnection(通过const+InstanceType方式使用)和DogConnection(通过继承方式使用),并验证 schema 中只注册了User、Dog、UserConnection、DogConnection` 等 5 个对象类型——两条使用路径殊途同归。

  3. 子类可新增字段:测试"adding new properties in child class"(L259-L405)中,RecipeEdge extends Edge(Recipe)新增personalNotes字段、FriendshipEdge extends Edge(User)新增friendedAt字段,schema 中两个类型字段数均为 3,且node字段分别正确指向Recipe与User。

  4. 子类可覆盖父类字段类型:测试"overwriting a property from base generic class in child class"(L407-L495)演示了子类用@Field() override baseField!: ChildSample;把字段从BaseSample覆盖为兼容的子类型ChildSample,最终 schema 中Child.baseField指向ChildSample——印证了文档中"overwrite the existing one's types"的说明。

此外,测试中beforeEach会调用getMetadataStorage().clear()清理元数据(L29-L31),说明泛型类型行为完全建立在元数据存储之上,与普通类型无本质区别。

两种工厂方式的选型建议

方式是否注册进 schema能否扩展字段是否需要唯一类型名适用场景
abstract类工厂 + 继承否(模板不注册)可以新增、覆盖字段否(子类有自己的类名)需要为每种实体定制包装类型(推荐)
非抽象类工厂(类型工厂)是(生成的类型直接注册)不推荐扩展必须动态生成唯一名称工厂调用一次、直接作为返回类型使用

两种方式各有定位:需要为不同实体"定制扩展"时用抽象工厂 + 继承;只需一次性生成可直接引用的类型时,用非抽象类型工厂并配合InstanceType。

总结

TypeGraphQL 的泛型类型能力,本质上是利用"函数工厂 + 抽象类 + 继承"在运行时模拟类型参数:函数参数承载运行时类型信息(供@Field解析),类型参数承载编译期类型信息(供 TypeScript 检查),两者一一对应。这一模式可复用于分页响应、连接(Connection/Edge)、通用包装类型等场景,与 类型继承 互为补充——继承解决"固定字段"复用,泛型工厂解决"可变字段类型"复用。源码 findType.ts、ClassType.ts、ObjectType.ts 以及 generic-types.ts 测试 共同佐证了上述行为,你可以在自己的项目中直接套用本文的三种写法。

  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载

相关推荐

上一篇:OpenResearch如何让证据随上下文走:日志、diff与制品关联机制完全指南
下一篇:Akagi麻将AI助手:专业玩家的实时分析与智能决策引擎

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询