ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂模式实现可复用的分页响应类型

TypeGraphQL 泛型类型(Generic Types)实战指南:用类工厂模式实现可复用的分页响应类型 后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载TypeGraphQL 提供了一套基于 TypeScript 类与装饰器的 GraphQL schema 构建方案。本文聚焦其中泛型类型Generic Types这一进阶特性由于 TypeScript 反射能力的限制装饰器无法直接与标准泛型类组合TypeGraphQL 通过类工厂class-creator模式让你能够像使用items: T[]这类类型参数一样构建出可复用的分页响应PaginatedResponse、连接Connection、边Edge等通用 GraphQL 类型。读完本文你将掌握工厂函数的写法、isAbstract与唯一类型名的取舍、复杂泛型值的类型签名以及如何在 resolver 中正确消费这些生成的类型。为什么需要泛型类型Type Inheritance类型继承 通过抽取公共字段到基类能够显著减少代码重复。但继承要求字段集合是严格固定的一个基类只能对应一组确定的字段类型。而真实业务中我们经常需要以类型参数的方式灵活声明某些字段的类型最典型的就是分页场景下的items: T[]——同样是列表 总数 是否还有更多的结构items的元素类型却随业务实体变化User、Recipe、Order……。TypeGraphQL 因此提供了对泛型 GraphQL 类型的支持用同一个工厂函数即可为任意实体生成对应的分页响应类型。核心限制与解决思路从源码结构看TypeScript 的装饰器与类型反射reflect-metadata只能基于具体类记录元数据标准泛型类如class FooT在运行时并没有任何可用的类型参数信息装饰器无法据此推断T对应的 GraphQL 类型。因此官方文档给出的方案是复用与 Resolvers Inheritance 中相同的类创建者class-creator模式——用一个普通函数充当类型工厂把泛型参数作为运行时参数传入函数内部动态创建并返回一个类。这样装饰器就能拿到真实的运行时值如User类本身来生成 schema。基本用法构建一个PaginatedResponse工厂第 1 步定义一个返回类的工厂函数先定义一个PaginatedResponse函数它创建并返回一个PaginatedResponseClassexport default function PaginatedResponse() { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }要让这个半成品具备泛型能力函数必须变成泛型函数并接收一个与类型参数相关的运行时实参export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }这里的ClassTypeTItem是 TypeGraphQL 暴露的类型工具其定义位于 src/typings/utils/ClassType.ts它表示一个构造器 prototype的组合即一个类的类型。这意味着你传入的TItemClass既是运行时值类本身又承载了编译期的类型信息prototype: T从而把类型参数与运行时参数绑定在一起。第 2 步为内部类添加装饰器给内部类加上合适的装饰器——可以是ObjectType、InterfaceType或InputType取决于你要生成的是对象类型、接口类型还是输入类型export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { ObjectType({ isAbstract: true }) abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }注意这里设置了isAbstract: true。这是必须的该抽象基类只是供子类继承的模板本身不应被注册进 schema。这一点有对应的测试佐证在 tests/functional/generic-types.ts 中BaseType抽象基类与SampleType具体子类同时存在时schema introspection 结果里SampleType正常出现且包含 2 个字段而BaseType则不会被输出断言baseTypeInfo为undefined。同类测试还覆盖了抽象InterfaceType与抽象InputType场景tests/functional/generic-types.ts。第 3 步像普通类一样声明字段但使用泛型类型与参数export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { // isAbstract 装饰器选项是必须的防止该基类被注册进 schema ObjectType({ isAbstract: true }) abstract class PaginatedResponseClass { // 这里使用运行时参数 Field(type [TItemClass]) // 这里使用泛型类型 items: TItem[]; Field(type Int) total: number; Field() hasMore: boolean; } return PaginatedResponseClass; }关键在于理解Field(type [TItemClass])中两处引用的分工type [TItemClass]是运行时参数TypeGraphQL 的 schema 生成器据此确定 GraphQL 字段类型[TItemClass!]!的列表而items: TItem[]是编译期类型保证返回值的 TypeScript 类型安全。两者由工厂函数的泛型参数TItem串联起来。第 4 步继承工厂结果创建专属类型ObjectType() class PaginatedUserResponse extends PaginatedResponse(User) { // 我们可以自由地添加更多字段或覆盖已有字段的类型 Field(type [String]) otherInfo: string[]; }PaginatedResponse(User)返回的抽象类被子类继承后User的字段类型会固化进PaginatedUserResponse。子类中还可以自由扩展新字段甚至用override关键字覆盖基类字段的类型测试 tests/functional/generic-types.ts 验证了覆盖为向上兼容类型后schema 中该字段正确指向新的对象类型。第 5 步在 Resolver 中使用Resolver() class UserResolver { Query() users(): PaginatedUserResponse { // 这里放置你的业务逻辑 // 取决于底层数据源和所用库 return { items, total, hasMore, otherInfo, }; } }返回对象的结构必须与类中所有Field声明的字段一一对应包括继承自抽象基类的items、total、hasMore以及子类新增的otherInfo。仓库实例分页返回Recipe列表仓库的 examples/generic-types 示例目录完整演示了这套流程其中工厂函数 paginated-response.type.ts 使用ObjectType()abstract class实现了文档所述模式该版本未显式写isAbstract因为抽象类不会被子类之外的代码实例化并注册export function PaginatedResponseTItemsFieldValue extends object( itemsFieldValue: ClassTypeTItemsFieldValue | string | number | boolean, ) { ObjectType() abstract class PaginatedResponseClass { Field(_type [itemsFieldValue]) items!: TItemsFieldValue[]; Field(_type Int) total!: number; Field() hasMore!: boolean; } return PaginatedResponseClass; }在 recipe.resolver.ts 中先通过继承工厂结果创建RecipesResponse类再在查询中返回它ObjectType() class RecipesResponse extends PaginatedResponse(Recipe) { // 需要更多字段时在这里添加 } Resolver() export class RecipeResolver { private readonly recipes createSampleRecipes(); 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, }; } }最终生成的 schema见 schema.graphql直观展示了效果——RecipesResponse类型带有完整的items: [Recipe!]!、total: Int!、hasMore: Boolean!字段且没有任何多余的抽象基类泄漏到 schema 中type Query { recipes(first: Int 10): RecipesResponse! } type RecipesResponse { hasMore: Boolean! items: [Recipe!]! total: Int! }复杂的泛型类型值不止类还能是标量前面的工厂参数是对象类型的类。但items也可能是string[]、number[]这类简单列表。此时需要放宽工厂函数的参数类型签名。关键在于工厂函数接收的参数本质上是能直接喂给Field装饰器的值。Field除了接受类还接受GraphQLScalarType、String、Number、Boolean等标量引用。因此可以这样写export default function PaginatedResponseTItemsFieldValue extends object( itemsFieldValue: ClassTypeTItemsFieldValue | GraphQLScalarType | String | Number | Boolean, ) { ObjectType({ isAbstract: true }) abstract class PaginatedResponseClass { Field(type [itemsFieldValue]) items: TItemsFieldValue[]; // ...其他字段 } return PaginatedResponseClass; }使用时传入对应的运行时值即可比如想返回字符串数组就传StringObjectType() class PaginatedStringsResponse extends PaginatedResponsestring(String) { // ... }示例目录中的 paginated-response.type.ts 也采用了类似思路不过把参数简化为ClassTypeTItemsFieldValue | string | number | boolean——这是因为在运行时String、Number、Boolean这些全局构造器本身就是可传给Field的有效引用。类型工厂Types Factory的另一种写法如果不希望使用isAbstract选项或abstract关键字也可以创建注册进 schema的类。但注意这种工厂生成的类型会被注册进 schema因此官方不推荐用这种方式扩展字段。更关键的是命名问题如果不做处理每次调用工厂都会生成名为PaginatedResponseClass的类型重复注册会导致 schema 生成错误。因此必须提供一个唯一的、动态生成的类型名export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { // 替代 isAbstract我们提供一个在 schema 中使用的唯一类型名 ObjectType(Paginated${TItemClass.name}Response) class PaginatedResponseClass { // 与前面代码片段相同的字段 } return PaginatedResponseClass; }ObjectType(name)的第一个参数支持字符串类型名这里用模板字符串拼出PaginatedUserResponse、PaginatedRecipeResponse这类唯一名称规避命名冲突。之后把生成的类存入变量。为了既能当运行时对象使用、又能当 TypeScript 类型使用需要同时声明一个同名类型const PaginatedUserResponse PaginatedResponse(User); type PaginatedUserResponse InstanceTypetypeof PaginatedUserResponse; Resolver() class UserResolver { // 记得给装饰器提供运行时类型参数 Query(returns PaginatedUserResponse) users(): PaginatedUserResponse { // 与前面代码片段相同的实现 } }注意两个细节Query(returns PaginatedUserResponse)中必须显式提供运行时类型参数这里指PaginatedResponse(User)的返回值因为PaginatedUserResponse的type别名在运行时并不存在仅靠 TS 类型推断无法让 TypeScript 反射到 GraphQL 类型InstanceTypetypeof PaginatedUserResponse从工厂产出的类类型中提取出实例类型使users()的返回类型注解与运行时值保持一致。测试 tests/functional/generic-types.ts 对这两种方式都做了验证Connection(User)配合consttype声明生成UserConnectionclass DogConnection extends Connection(Dog) {}生成DogConnection两者在 schema 中都能正确注册且items字段分别解析为User与Dog对象类型。小结与建议泛型类型让 TypeGraphQL 得以在装饰器 反射的限制下复刻 TypeScript 泛型语义模板与实现的分离工厂函数内的抽象基类只承载公共结构isAbstract: true防止泄漏进 schema子类继承时才固化具体元素类型运行时与编译期双通道ClassTypeTItem同时携带运行时类与编译期类型Field(type [TItemClass])提供运行时类型items: TItem[]保证编译期类型安全两种消费方式需要扩展字段时用继承工厂结果仅需开箱即用时用变量 InstanceType但必须为ObjectType提供唯一类型名可验证性上述行为均由 tests/functional/generic-types.ts 中的 introspection 断言与真实 query 执行测试覆盖完整可运行示例见 examples/generic-types包含 index.ts 引导脚本、examples.graphql 查询示例与 schema.graphql 生成结果。实际项目中建议优先采用isAbstract: true 子类继承的写法它既能把items、total、hasMore这类分页骨架做成全项目复用的模板又能在每个业务子类中自由添加字段是构建分页、连接Connection/Edge类似 Relay 风格等通用类型的推荐实践。赞分享后端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 0.17.0后端GraphQLAPI设计TypeGraphQL 泛型类型Generic Types实战用类工厂模式构建可复用的分页响应与连接类型TypeGraphQL 泛型类型Generic Types实战用类工厂模式构建可复用的分页响应与连接类型 导读 本文聚焦 TypeGraphQL 的泛型类后端GraphQLAPI设计HumanLayer 本地工具链解析用 /iterate_plan_nt 迭代实现方案的工作流设计HumanLayer 本地工具链解析用 /iterate_plan_nt 迭代实现方案的工作流设计 在 Claude Code 驱动的开发流程中实现方案I后端GraphQLAPI设计上一篇如何永久保存你的微信聊天记忆WeChatMsg开源工具终极指南下一篇如何突破NCM格式限制ncmdump工具全攻略创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表