实战指南:用类工厂实现可复用的 `PaginatedResponse` 等模式)
后端GraphQLAPI设计【免费下载链接】type-graphqlCreate 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 PaginatedResponseT { items: T[] }的标准泛型类其items字段的design:type只会是Object装饰器拿不到真实的元素类型同时装饰器求值发生在类定义时此时类型参数尚未实例化也无法与具体类绑定。因此官方文档明确指出装饰器无法与标准泛型类直接组合只能借用类创建器class-creator模式——即与 Resolver 继承 中createBaseResolver工厂函数相同的思路。基础用法abstract 类工厂 继承第一步定义类工厂函数先把返回类型封装成一个函数函数内部创建并返回一个abstract类export default function PaginatedResponse() { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }第二步让工厂接收类型参数对应的运行时值要获得泛型行为工厂函数本身必须是泛型函数并接收一个与类型参数相关的运行时参数也就是实际要作为字段类型的类export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }这里的ClassTypeTItem是 TypeGraphQL 提供的工具类型其定义位于 ClassType.tsexport type ClassTypeT extends object object, Arguments extends unknown[] any[] Constructor T, Arguments { prototype: T; };它表示能以T为实例类型被构造的类既描述了运行时能拿到的类对象也约束了类型参数的形状。第三步给内部类添加装饰器工厂返回的类可以像普通类一样使用ObjectType、InterfaceType或InputType装饰器取决于你要把它用作输出对象类型、接口还是输入类型export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { ObjectType() abstract class PaginatedResponseClass { // ... } return PaginatedResponseClass; }第四步像普通类一样声明字段字段声明与常规类型完全一致唯一区别是类型参数TItem与运行时参数TItemClass配对使用——Field的返回类型函数引用运行时值属性类型使用类型参数export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { 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 PaginatedResponseTItemsFieldValue extends object( itemsFieldValue: ClassTypeTItemsFieldValue | GraphQLScalarType | String | Number | Boolean, ) { ObjectType() abstract class PaginatedResponseClass { Field(type [itemsFieldValue]) items: TItemsFieldValue[]; // ... 其他字段 } return PaginatedResponseClass; }使用时传入对应的运行时值ObjectType() class PaginatedStringsResponse extends PaginatedResponsestring(String) { // ... }注意类型参数与运行时值必须匹配PaginatedResponsestring(String)中泛型参数string描述属性类型运行时值String告诉 schema 生成器字段的 GraphQL 类型。类型工厂免继承的直接注册方式前文方案依赖abstract类 继承。TypeGraphQL 还提供另一种方式不写abstract让工厂直接生成可注册的类型类。但有两点必须注意类型会被注册进 schema非抽象类工厂产生的类型会作为独立类型出现在 schema 中因此不推荐用这种方式再扩展字段必须提供唯一的类型名工厂每次调用都会生成同名类如PaginatedResponseClass如果同一个工厂被多次实例化schema 中会出现重复的类型名并导致构建错误。解决办法是用ObjectType的第一个参数动态生成类型名export default function PaginatedResponseTItem extends object(TItemClass: ClassTypeTItem) { // 提供在 schema 中使用的唯一类型名 ObjectType(Paginated${TItemClass.name}Response) class PaginatedResponseClass { // ... } return PaginatedResponseClass; }这样PaginatedResponse(User)会生成名为PaginatedUserResponse的 GraphQL 类型PaginatedResponse(Recipe)则生成PaginatedRecipeResponse互不冲突。ObjectType的第一个参数即类型名其签名可见于 ObjectType.tsObjectType(name: string, options?)。随后把生成的类存进变量并同时创建同名的类型别名才能在 Resolver 中既当运行时对象又当类型使用const PaginatedUserResponse PaginatedResponse(User); type PaginatedUserResponse InstanceTypetypeof PaginatedUserResponse; Resolver() class UserResolver { // 给装饰器提供运行时类型参数 Query(returns PaginatedUserResponse) users(): PaginatedUserResponse { // 实现同前 } }const PaginatedUserResponse运行时值用于Query(returns ...)装饰器type PaginatedUserResponse InstanceTypetypeof PaginatedUserResponse类型别名用于方法返回类型标注。仓库示例与运行验证仓库 examples/generic-types 提供了完整的可运行示例与文档中的抽象工厂方案一致paginated-response.type.ts定义PaginatedResponse工厂内部为ObjectType()抽象的PaginatedResponseClass字段为items列表、totalInt、hasMoreBoolean。参数类型为ClassTypeTItemsFieldValue | string | number | boolean对应文档中复杂泛型值的思路recipe.type.ts实体Recipe类型recipe.resolver.ts创建ObjectType() class RecipesResponse extends PaginatedResponse(Recipe)并在Query中返回分页结果同时提供一个addSampleRecipeMutationrecipe.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测试shouldnt emit unused abstract object typeL33-L66证明只有ObjectType() abstract class BaseType被继承、但自身未被引用时introspection 结果中不存在BaseType只有SampleType。这是抽象类工厂能安全用作模板的前提。多子类共享同一工厂测试multiple children of base generic classL148-L257用非抽象工厂ConnectionTItem 动态类型名ObjectType(\${TItemClass.name}Connection)同时生成UserConnection通过constInstanceType方式使用和DogConnection通过继承方式使用并验证 schema 中只注册了User、Dog、UserConnection、DogConnection 等 5 个对象类型——两条使用路径殊途同归。子类可新增字段测试adding new properties in child classL259-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 classL407-L495演示了子类用Field() override baseField!: ChildSample;把字段从BaseSample覆盖为兼容的子类型ChildSample最终 schema 中Child.baseField指向ChildSample——印证了文档中overwrite the existing ones types的说明。此外测试中beforeEach会调用getMetadataStorage().clear()清理元数据L29-L31说明泛型类型行为完全建立在元数据存储之上与普通类型无本质区别。两种工厂方式的选型建议方式是否注册进 schema能否扩展字段是否需要唯一类型名适用场景abstract类工厂 继承否模板不注册可以新增、覆盖字段否子类有自己的类名需要为每种实体定制包装类型推荐非抽象类工厂类型工厂是生成的类型直接注册不推荐扩展必须动态生成唯一名称工厂调用一次、直接作为返回类型使用两种方式各有定位需要为不同实体定制扩展时用抽象工厂 继承只需一次性生成可直接引用的类型时用非抽象类型工厂并配合InstanceType。总结TypeGraphQL 的泛型类型能力本质上是利用函数工厂 抽象类 继承在运行时模拟类型参数函数参数承载运行时类型信息供Field解析类型参数承载编译期类型信息供 TypeScript 检查两者一一对应。这一模式可复用于分页响应、连接Connection/Edge、通用包装类型等场景与 类型继承 互为补充——继承解决固定字段复用泛型工厂解决可变字段类型复用。源码 findType.ts、ClassType.ts、ObjectType.ts 以及 generic-types.ts 测试 共同佐证了上述行为你可以在自己的项目中直接套用本文的三种写法。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐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设计上一篇OpenResearch如何让证据随上下文走日志、diff与制品关联机制完全指南下一篇Akagi麻将AI助手专业玩家的实时分析与智能决策引擎创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考