恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂模式构建可复用的分页响应类型
首页
资讯中心
/
TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂模式构建可复用的分页响应类型
TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂模式构建可复用的分页响应类型
发布时间:2026/9/27 21:35:08
后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载本篇文章以 TypeGraphQL 0.17.0 版本文档website/versioned_docs/version-0.17.0/generic-types.md为主线系统讲解 TypeGraphQL 如何借助 TypeScript 的「类工厂class factory」模式描述泛型 GraphQL 类型。你将掌握PaginatedResponseTItem这类泛型响应类型的完整写法、isAbstract与唯一类型名的取舍、非类类型参数的扩展技巧并通过仓库内示例与测试用例验证其底层行为。读完即可在自己的 Resolver 中落地可复用的分页、连接Connection等泛型类型。为什么需要泛型类型从类型继承说起TypeGraphQL 的核心思路是用 TypeScript 类来描述 GraphQL 类型。面向对象编程中「类型继承」是消除重复代码的常用手段——把公共字段提取到基类再让子类继承例如 类型继承文档 中演示的Person - Student模式ObjectType() class Person { Field() age: number; } ObjectType() class Student extends Person { Field() universityName: string; }但继承只能解决「字段集合固定」的场景。实际业务中我们常常需要更灵活的类型描述比如分页场景下的items: T[]——其中T是一个类型参数可以是User、Recipe或任意其他类型。固定的基类无法表达这种变化这正是 TypeGraphQL 提供「泛型 GraphQL 类型」支持的原因。核心原理为什么标准泛型类行不通一个自然的想法是直接写 TypeScript 泛型类ObjectType() abstract class PaginatedResponseTItem { Field(type [TItem]) // ← 反射无法还原 TItem items: TItem[]; }遗憾的是TypeScript 的反射能力有限装饰器接收到的类型信息来自design:type元数据而泛型参数在运行时已被擦除TItem无法被反射机制还原为具体的 GraphQL 类型。因此 TypeGraphQL 无法直接支持带装饰器的标准泛型类。解决思路与文档 resolvers inheritance 中描述的「类创建器class-creator模式」一致编写一个接收运行时参数并返回类的工厂函数把类型参数转化为实实在在的运行时值类本身从而绕开反射限制。从源码看TypeGraphQL 对「类的构造器」类型有明确定义src/typings/utils/ClassType.ts 中ClassTypeT即「可构造出T实例的构造器函数」它正是泛型工厂函数参数的标准类型export type ClassTypeT extends object object, Arguments extends unknown[] any[] ConstructorT, Arguments { prototype: T };如何实现类工厂模式五步走下面以最常见的「分页响应」为例逐步搭建泛型类型。文档完整示例见 examples/generic-types。第一步定义类工厂函数先定义一个PaginatedResponse函数它创建并返回一个PaginatedResponseClassexport default function PaginatedResponse() { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }第二步让函数泛型化并接收类型参数的运行时值要让行为「泛型化」函数本身必须是泛型并且接收与类型参数相关的运行时参数即真实存在的类export default function PaginatedResponseTItem(TItemClass: ClassTypeTItem) { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }这里的TItemClass就是TItem的运行时化身后面Field装饰器会直接使用它来推断 GraphQL 类型。第三步给返回的类添加装饰器并声明isAbstract返回的类可以装饰为ObjectType、InterfaceType或InputType取决于泛型类型将被用作输出类型、接口还是输入类型。关键点必须设置isAbstract: true防止工厂类本身被注册进 schemaexport default function PaginatedResponseTItem(TItemClass: ClassTypeTItem) { ObjectType({ isAbstract: true }) abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }为什么isAbstract: true是必需的从测试 tests/functional/generic-types.ts 的断言可以验证被标记为 abstract 的基类不会作为独立类型出现在 schema 内expect(baseTypeInfo).toBeUndefined()而它的字段会完整合并进子类。也就是说抽象类只是「字段容器」真正进入 GraphQL schema 的是继承它的具体子类。同时由于工厂每次调用都会产生一个新的类若不标记 abstract多次调用PaginatedResponse(User)会产生多个同名的PaginatedResponseClass导致 schema 构建时出现类型名重复错误。第四步像普通类一样声明字段但使用泛型参数在类内部可以正常写Field其中「运行时参数」用于装饰器推断类型「泛型类型」用于 TypeScript 编译期类型检查export default function PaginatedResponseTItem(TItemClass: ClassTypeTItem) { // isAbstract decorator option is mandatory to prevent registering in schema ObjectType({ isAbstract: true }) abstract class PaginatedResponseClass { // here we use the runtime argument Field(type [TItemClass]) // and here the generic type items: TItem[]; Field(type Int) total: number; Field() hasMore: boolean; } return PaginatedResponseClass; }注意Field(type [TItemClass])表示items是「TItemClass类型元素的数组」total用Int标量hasMore默认推断为Boolean。这样工厂类就同时具备了运行时字段定义与编译期类型约束。第五步实例化具体类型并在 Resolver 中使用最后调用工厂函数生成专属于某个类型的子类还可以自由追加字段甚至覆盖基类字段的类型ObjectType() class PaginatedUserResponse extends PaginatedResponse(User) { // we can freely add more fields or overwrite the existing ones types Field(type [String]) otherInfo: string[]; }然后在 Resolver 中把它当作普通类型使用Resolver() class UserResolver { Query() users(): PaginatedUserResponse { const response new PaginatedUserResponse(); // here is your custom business logic, // depending on underlying data source and libraries return response; } }仓库中的 examples/generic-types/recipe.resolver.ts 给出了真实可运行的版本——它用RecipesResponse extends PaginatedResponse(Recipe)生成类型查询支持first参数并返回分页数据Query({ name: recipes }) getRecipes( Arg(first, _type Int, { nullable: true, defaultValue: 10 }) first: number, ): RecipesResponse { const total this.recipes.length; return { items: this.recipes.slice(0, first), hasMore: total first, total, }; }对应生成的 schemaexamples/generic-types/schema.graphql证实了泛型展开的结果——RecipesResponse的items被正确解析为[Recipe!]!type Query { recipes(first: Int 10): RecipesResponse! } type RecipesResponse { hasMore: Boolean! items: [Recipe!]! total: Int! }进阶非类类型的泛型参数复杂泛型类型值上面的TItemClass参数类型是ClassTypeTItem它要求传入一个类对象类型。但某些场景下items的元素并不是类而是标量——例如想要一个items: string[]的分页响应。此时需要放宽工厂函数签名。本质规律是工厂函数接收的参数就是你可以传给Field装饰器的值。因此参数类型可以扩展为GraphQLScalarType、String、Number、Boolean等export default function PaginatedResponseTItemsFieldValue extends object( itemsFieldValue: ClassTypeTItemsFieldValue | GraphQLScalarType | String | Number | Boolean, ) { ObjectType() abstract class PaginatedResponseClass { Field(type [itemsFieldValue]) items: TItemsFieldValue[]; // ... Other fields } return PaginatedResponseClass; }调用时传入对应的运行时标量值即可ObjectType() class PaginatedStringsResponse extends PaginatedResponsestring(String) { // ... }在 examples/generic-types/paginated-response.type.ts 中仓库实际采用的是ClassTypeTItemsFieldValue | string | number | boolean的签名组合同样覆盖了「标量数组分页」的用法读者可对照参考。进阶类型工厂不推荐与唯一类型名你也可以不写isAbstract选项、也不用abstract关键字直接生成一个「真实注册」的泛型类。但这样做出来的类型会被注册进 schema因此不推荐用它来扩展类型、追加额外字段会产生多余的 schema 类型。若仍要采用这种写法为避免 schema 报「PaginatedResponseClass类型名重复」的错误必须提供唯一的、根据类型参数生成的名称export default function PaginatedResponseTItem(TItemClass: ClassTypeTItem) { // instead of isAbstract, you have to provide a unique type name used in schema ObjectType({ name: Paginated${TItemClass.name}Response }) class PaginatedResponseClass { // the same fields as in the earlier code snippet } return PaginatedResponseClass; }从 src/decorators/ObjectType.ts 的源码可以看到ObjectType重载既支持ObjectType(options)也支持ObjectType(name, options)的字符串形式当不传 name 时默认取target.namename: name || target.name这正解释了为什么每次调用工厂都必须显式传唯一名称否则所有实例都会撞名PaginatedResponseClass。随后可以把生成的类存进变量。为了让同一个名字既能当运行时对象又能当 TS 类型需要额外声明一个InstanceType类型const PaginatedUserResponse PaginatedResponse(User); type PaginatedUserResponse InstanceTypetypeof PaginatedUserResponse; Resolver() class UserResolver { // remember to provide a runtime type argument to the decorator Query(returns PaginatedUserResponse) users(): PaginatedUserResponse { // the same implementation as in the earlier code snippet } }注意这里Query(returns PaginatedUserResponse)必须传入运行时类型参数闭包返回值不能只依赖方法返回值的 TS 类型注解——因为反射在编译后读不到它。源码与测试验证泛型类型的行为证据除了上面的示例仓库的 tests/functional/generic-types.ts 从三个维度固化了该特性的行为可作为理解底层原理的第一手材料abstract 类型不进 schema测试断言被标记为 abstract 的基类无论ObjectType、InterfaceType还是InputType都不会出现在 schema 的 introspection 结果中而子类会继承其全部字段expect(baseTypeInfo).toBeUndefined()、expect(sampleTypeInfo.fields).toHaveLength(2)。同一工厂的多个子类可共存ConnectionTItem工厂同时派生出UserConnection与DogConnectionschema 中两者都正确注册items分别解析为User与Dog同时测试覆盖了「const 变量 InstanceType」与「class 继承」两种使用语法。子类可新增与覆盖字段EdgeTNode工厂派生的RecipeEdge与FriendshipEdge各自追加了personalNotes、friendedAt字段Child extends Base(BaseSample)中甚至用override baseField!: ChildSample把基类泛型字段覆盖为类型兼容的子类型schema 中baseField最终指向ChildSample。使用建议与注意事项优先使用isAbstract: trueabstract关键字这是文档推荐的主方案工厂类不会污染 schema子类扩展自由。避免非 abstract 工厂除非你确实需要每个实例作为独立 schema 类型否则不要用它做字段扩展若使用务必通过name生成唯一类型名。类型参数必须「运行时化」泛型工厂的每个类型参数都要对应一个运行时值类或标量否则装饰器无法推断 GraphQL 类型。类型与值要成对声明采用const X Factory(T); type X InstanceTypetypeof X模式时Query(returns X)必须传入运行时值。同一工厂多次调用会注册多类型在非 abstract 模式下多次调用会注册多个同名类并报错因此唯一名称是硬性要求。掌握这一模式后分页响应、Connection 边Edge等泛型结构都可以沉淀为可复用的工厂函数配合 类型继承 一起使用能大幅压缩 GraphQL schema 定义的重复代码。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL 泛型类型Generic Types实战用类工厂模式构建可复用的分页响应与连接类型TypeGraphQL 泛型类型Generic Types实战用类工厂模式构建可复用的分页响应与连接类型 导读 本文聚焦 TypeGraphQL 的泛型类后端GraphQLAPI设计如何实现 Developer Portfolio 企业级部署AWS、DigitalOcean 和 CI/CD 流水线终极指南如何实现 Developer Portfolio 企业级部署AWS、DigitalOcean 和 CI/CD 流水线终极指南 在当今数字时代拥有一个专业的开TypeScript 映射类型Mapped Types实战指南用 keyof 与泛型批量变换对象类型TypeScript 映射类型Mapped Types实战指南用 keyof 与泛型批量变换对象类型 本篇指南基于开源仓库《The Concise Typ文档教程上一篇SchemaCrawler代码质量检查10个必备的数据库设计lint规则下一篇UnattendGenerator实战案例如何批量部署Windows系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考