- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
导读
在 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,让工厂直接生成可注册的类型类。但有两点必须注意:
- 类型会被注册进 schema:非抽象类工厂产生的类型会作为独立类型出现在 schema 中,因此不推荐用这种方式再扩展字段;
- 必须提供唯一的类型名:工厂每次调用都会生成同名类(如
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 测试 从多个维度验证了这套模式的底层行为,可作为理解原理的实证:
抽象类不会注册进 schema:测试"shouldn't emit unused abstract object type"(L33-L66)证明,只有
@ObjectType() abstract class BaseType被继承、但自身未被引用时,introspection 结果中不存在BaseType,只有SampleType。这是抽象类工厂能安全用作模板的前提。多子类共享同一工厂:测试"multiple children of base generic class"(L148-L257)用非抽象工厂
Connection<TItem>+ 动态类型名@ObjectType(\${TItemClass.name}Connection`),同时生成UserConnection(通过const+InstanceType方式使用)和DogConnection(通过继承方式使用),并验证 schema 中只注册了User、Dog、UserConnection、DogConnection` 等 5 个对象类型——两条使用路径殊途同归。子类可新增字段:测试"adding new properties in child class"(L259-L405)中,
RecipeEdge extends Edge(Recipe)新增personalNotes字段、FriendshipEdge extends Edge(User)新增friendedAt字段,schema 中两个类型字段数均为 3,且node字段分别正确指向Recipe与User。子类可覆盖父类字段类型:测试"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!
相关推荐
TypeGraphQL 泛型类型实战:用类工厂模式实现可复用的 PaginatedResponse 等通用 GraphQL 类型
TypeGraphQL 泛型类型实战:用类工厂模式实现可复用的 PaginatedResponse 等通用 GraphQL 类型 导读 在 TypeGraphQ
后端GraphQLAPI设计TypeGraphQL 泛型类型(Generic Types)实战:用类工厂模式打造可复用的分页响应等泛型 GraphQL 类型
TypeGraphQL 泛型类型(Generic Types)实战:用类工厂模式打造可复用的分页响应等泛型 GraphQL 类型 TypeGraphQL 的 类
后端GraphQLAPI设计TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂模式实现可复用的分页响应类型
TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂模式实现可复用的分页响应类型 TypeGraphQL 提供了一套基于 TypeS
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考