TypeGraphQL 接口(Interface)类型实战指南:用抽象类定义 GraphQL 接口并实现类型继承
后端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。本指南聚焦于文档 website/versioned_docs/version-2.0.0-rc.1/interfaces.md 所讲解的 GraphQL 接口类型如何使用InterfaceType装饰器、如何在对象类型中实现接口、如何让接口继承接口、如何在接口字段上声明参数与解析器以及如何控制接口实现类型在 Schema 中的注册行为。读完本文你将掌握用 TypeScript 类完整构建 GraphQL 接口体系含 Relay 风格Node接口的实战方案并能从源码层面理解其底层工作方式。为什么需要接口从 TypeScript interface 到抽象类在面向对象编程中接口Interface用于描述实现它的类必须遵守的契约。GraphQL 同样提供了 Interface Type用于抽象出一组对象类型共享的字段形状。TypeGraphQL 支持定义 GraphQL 接口其基本思路延续了项目的核心设计——用 TypeScript 类驱动 Schema 生成。这里存在一个天然障碍TypeScript 的interface拥有一流支持但它们只存在于编译期运行时会被擦除。装饰器Decorator需要在运行时读取元数据来构建 Schema因此无法直接使用 TypeScript 的interface作为 Schema 定义的载体。解决方案是使用抽象类abstract class抽象类不能被实例化但可以被其他类实现或继承行为上几乎等同于接口。唯一差异在于抽象类不会强制阻止开发者漏实现某个方法或漏初始化某个字段——因此只要开发团队自律地将抽象类当作接口使用就可以安全地用它来构建 GraphQL 接口。这一思路也贯穿于仓库的测试与示例中例如 tests/functional/interfaces-and-inheritance.ts 中的SampleInterface1、SampleInterface2等均以抽象类形式定义。定义接口类型创建 GraphQL 接口定义的方式与对象类型非常接近创建一个抽象类并用InterfaceType()装饰器标注它字段部分则与对象类型完全一致使用Field声明类型形状InterfaceType() abstract class IPerson { Field(type ID) id: string; Field() name: string; Field(type Int) age: number; }对应到源码层面InterfaceType装饰器src/decorators/InterfaceType.ts支持三种重载签名无参调用InterfaceType()、传配置对象InterfaceType(options)、传名称与配置InterfaceType(name, options)。装饰器内部调用getMetadataStorage().collectInterfaceMetadata(...)将接口的name默认取类名target.name、target、interfaceClasses等信息收集到全局元数据存储中供后续 Schema 生成器使用export function InterfaceType( nameOrOptions?: string | InterfaceTypeOptions, maybeOptions?: InterfaceTypeOptions, ): ClassDecorator { const { name, options } getNameDecoratorParams(nameOrOptions, maybeOptions); const interfaceClasses options.implements ([] as Function[]).concat(options.implements); return target { getMetadataStorage().collectInterfaceMetadata({ name: name || target.name, target, interfaceClasses, autoRegisteringDisabled: options.autoRegisterImplementations false, ...options, }); }; }其中InterfaceTypeOptions由DescriptionOptions、ResolveTypeOptions、ImplementsClassOptions以及autoRegisterImplementations组合而成见 src/decorators/types.ts。各选项的职责如下选项类型作用namestring覆盖默认的接口 Schema 名称默认使用类名descriptionstring接口的 Schema 描述descriptionimplementsFunction \| Function[]声明该接口实现的其他接口要求 graphql-js ≥ 15.0resolveTypeTypeResolver自定义运行时的具体类型解析函数autoRegisterImplementationsboolean是否自动把实现该接口的所有对象类型注册进 Schema默认true对象类型实现接口定义好接口类型类之后就可以像使用 TypeScript 接口一样把它挂到对象类型上。唯一的区别在于需要让 TypeGraphQL 知道该ObjectType实现了某个InterfaceType做法是在装饰器中传入implements选项ObjectType({ implements: IPerson }) class Person implements IPerson { id: string; name: string; age: number; }如果同时实现多个接口传入数组即可ObjectType({ implements: [IPerson, IAnimal, IMachine] }) class Human implements IPerson, IAnimal, IMachine { // ... }这里有一个非常实用的特性实现类上可以省略字段装饰器。因为实现类的 GraphQL 字段会直接从接口定义中复制无需在对象类型中重复维护Field此时可以完全依赖 TypeScript 的类型检查来保证接口实现正确避免两处定义不一致的问题。这一点在仓库示例 examples/interfaces-inheritance/person/person.type.ts 中得到了体现——Person类的id、name、age字段都没有重复标注Field而 Schema 生成结果中见 examples/interfaces-inheritance/schema.graphqltype Person implements IPerson依然完整携带了id: ID!、name: String!、age: Int!这些字段。接口的字段同样会被继承到子类中并进入 Schema。因此你也可以让对象类型直接extends接口抽象类仅补充自己的字段ObjectType({ implements: IPerson }) class Person extends IPerson { Field() hasKids: boolean; }两种写法implements或extends均可取决于你是想从零实现字段还是想直接继承接口类已有的字段实现。接口实现其他接口构建多层级接口体系自graphql-js15.0 起GraphQL 接口类型也可以实现其他接口类型。在 TypeGraphQL 中语法与对象类型一致——同样使用implements装饰器选项InterfaceType() class Node { Field(type ID) id: string; } InterfaceType({ implements: Node }) class Person extends Node { Field() name: string; Field(type Int) age: number; }注意此处的细节接口类Person既用extends Node在 TypeScript 层面继承了字段又用implements: Node在 GraphQL 层面声明了接口继承关系。二者是互补的——TypeScript 的继承负责复用实现implements负责生成 GraphQL SDL 中的implements声明。当实现一个已经实现了其他接口的接口时不需要把整条继承链上的所有接口都写进implements数组只需声明继承链上最近的那一个。例如ObjectType({ implements: [Person] }) class Student extends Person { Field() universityName: string; }Student只需写implements: [Person]TypeGraphQL 会依据Person的接口继承关系自动推导出它同样实现了Node。上述三段代码最终生成的 GraphQL SDL 如下interface Node { id: ID! } interface Person implements Node { id: ID! name: String! age: Int! } type Student implements Node Person { id: ID! name: String! age: Int! universityName: String! }可以看到type Student在 SDL 中自动带上了implements Node Person的完整声明。这种只写最近一级、自动补齐整条链的行为在 tests/functional/interfaces-and-inheritance.ts 中被系统化地验证测试中同时覆盖了单接口实现SampleImplementingObject1、多接口实现SampleMultiImplementingObject同时implements [SampleInterface1, SampleInterface2]、接口继承接口SampleInterfaceImplementing1通过implements: [SampleInterface1]实现SampleInterface1以及类继承与接口实现的叠加SampleExtendingImplementingObject extends SampleImplementingObject2等多种组合是理解该行为的绝佳测试素材。接口字段的解析器与参数接口不只是静态字段的集合它还允许定义字段解析器与参数。方法级字段解析器直接在类的方法上标注Field在接口中同样可用语法与对象类型完全一致InterfaceType() abstract class IPerson { Field() firstName: string; Field() lastName: string; Field() fullName(): string { return ${this.firstName} ${this.lastName}; } }这些方法级解析器会被所有实现该接口的对象类型继承——只要对象类型没有为这些字段提供自己的解析器实现就会沿用接口中的默认实现。在接口字段上声明参数接口字段可以声明参数。例如希望在 SDL 中表达下面的形状interface IPerson { avatar(size: Int!): String! }可以直接在接口类的方法中使用Arg或Args装饰器用法与在对象类型或 resolver 中完全一致InterfaceType() abstract class IPerson { Field() avatar(Arg(size) size: number): string { return http://i.pravatar.cc/${size}; } }抽象方法的两难与解法这里有一个 TypeScript 的限制TypeScript 不允许在抽象方法上使用装饰器。因此如果你只想要一个签名契约强制参数与返回类型而不想或无法提供默认实现就不能用abstract关键字而必须在方法体内抛出错误InterfaceType() abstract class IPerson { Field() avatar(Arg(size) size: number): string { throw new Error(Method not implemented!); } }随后所有实现该接口的对象类型都必须extends接口类并覆写该方法提供真正的实现这一覆写是所有实现该接口的对象类型的硬性要求ObjectType({ implements: IPerson }) class Person extends IPerson { avatar(size: number): string { return http://i.pravatar.cc/${size}; } }仓库示例 examples/interfaces-inheritance/person/person.interface.ts 正是这一模式的实战范本IPerson.avatar声明了Arg(size)参数并抛出new Error(Method not implemented.)而Personperson.type.ts覆写后返回真实的头像 URL。从生成的 schema.graphql 可见接口字段avatar(size: Float!): String!同时出现在interface IPerson与type Person implements IPerson中证明该模式工作正常。扩展签名为字段增加额外参数如果对象类型想在接口契约的基础上扩展签名——比如为avatar增加format参数——需要重新声明整个字段签名而不能只追加一个参数。注意此时必须使用implements IPerson而非extends因为你需要自定义方法体而不再是继承ObjectType({ implements: IPerson }) class Person implements IPerson { Field() avatar(Arg(size) size: number, Arg(format) format: string): string { return http://i.pravatar.cc/${size}.${format}; } }用 FieldResolver 在 resolver 类层面解析接口字段除了在类方法上定义解析器接口字段的解析器也可以在 resolver 类层面定义使用FieldResolver装饰器。用Resolver(of IPerson)绑定接口类型再通过Root()拿到接口实例Resolver(of IPerson) class IPersonResolver { FieldResolver() avatar(Root() person: IPerson, Arg(size) size: number): string { return http://typegraphql.com/${person.id}/${size}; } }这种写法把接口字段的解析逻辑与类型定义解耦适合字段计算较复杂、或在多个对象类型间共享统一解析逻辑的场景。与之配套的专项测试 tests/functional/interface-resolvers-args.ts 覆盖了接口字段带参数 resolver 类级解析的完整链路可作参考。控制接口实现类型在 Schema 中的注册默认情况下只要接口类型被显式用于 Schema例如作为某个 Query/Mutation 的返回类型或某个字段的类型所有实现了该接口的对象类型都会被自动发射emit进 Schema无需任何额外操作。这一自动注册行为在 src/schema/schema-generator.ts 的buildOtherTypes方法中实现生成器遍历objectTypesInfoMap凡是实现了某个接口、且该接口未被禁用自动注册、且该接口确实被使用的对象类型都会进入autoRegisteredObjectTypesInfo并加入最终 Schema。不过在某些场景下这种自动行为并不理想。典型例子是 Relay 体系中的Node接口当对外暴露多个相互独立的 Schema比如一个公开 Schema 和一个私有 Schema时你可能并不希望一个接口把它的所有实现类型全部带进每个 Schema。此时可以通过InterfaceType的autoRegisterImplementations: false选项关闭自动注册InterfaceType({ autoRegisterImplementations: false }) abstract class Node { Field(type ID) id: string; }关闭自动注册后需要在buildSchema的orphanedTypes数组中手动列出想要暴露在指定 Schema 中的实现类型const schema await buildSchema({ resolvers, // 手动提供孤儿对象类型 orphanedTypes: [Person, Animal, Recipe], });在源码层面autoRegisterImplementations: false会被转换为autoRegisteringDisabled: true存入接口元数据src/decorators/InterfaceType.tsSchema 生成器在自动注册过滤逻辑中会据此跳过该接口的实现类型src/schema/schema-generator.ts。同时要留意一个边界情况如果某个对象类型类被显式用作 GraphQL 类型例如Recipe作为addRecipemutation 的返回类型无论orphanedTypes如何设置它都会被发射进 Schema——orphanedTypes只影响那些未被显式引用的类型。解析具体类型resolveType 的正确用法当对象类型实现 GraphQL 接口后在 resolver 中必须返回类型类的实例而不是普通对象字面量。因为graphql-js需要借助运行时对象所属的类来探测其底层 GraphQL 类型如果返回的是无类型信息的纯对象graphql-js将无法正确判定具体类型。以 examples/interfaces-inheritance/resolver.ts 为例addStudentmutation 中先Object.assign(new Student(), {...})构造出真实的类实例再返回Querypersons返回的IPerson[]数组里存放的也全部是Student/Employee的真实实例——这正是为了让graphql-js能在运行时通过value.constructor识别出具体类型。如果你希望返回普通对象plain object也可以通过InterfaceType的resolveType选项提供自定义的类型判定函数——通过检查数据对象的形状来决定其具体类型思路与联合类型Union一致参见 unions 文档InterfaceType({ resolveType: value { if (grades in value) { return Student; // 返回类型的 Schema 名称字符串 } return Person; // 或直接返回对象类型类 }, }) abstract class IPerson { // ... }resolveType函数既可以返回类型的 Schema 名称字符串如Student也可以返回对象类型类本身如Person二者等价。不过相比 Union接口场景下的resolveType要更棘手一些实现某个接口的对象类型可能很多开发者未必能一一记住并写出完备的类型判定分支。因此更稳妥的默认做法仍是在 resolver 中返回真实类型类实例让框架自行探测。仓库示例 examples/interfaces-inheritance/person/person.interface.ts 还展示了一种简化技巧——resolveType: value value.constructor.name直接利用构造函数的类名作为类型名返回规避了手写判定逻辑但要求实现类的类名与 Schema 类型名保持一致。综合示例完整的接口继承项目如果希望看到接口与类型继承的更高级用法——例如 Query 直接返回接口类型persons: [IPerson!]!、mutation 返回具体实现类型、输入类型InputType与对象类型之间共享字段结构——仓库提供了完整的可运行示例位于 examples/interfaces-inheritanceperson/person.interface.ts定义IPerson接口含带参字段avatar(size: Int!): String!与resolveTypeperson/person.type.tsPerson实现IPerson接口字段不重复标Fieldstudent/student.type.ts 与 employee/employee.type.tsStudent、Employee继承Person并各自补充字段resolver.ts返回接口数组的 Query 与返回具体类型的 Mutationschema.graphql最终生成的完整 SDL直观展示interface IPerson与type Person/Student/Employee implements IPerson的对应关系。小结围绕 TypeGraphQL 的接口支持本文完整覆盖了从概念到源码的各个环节用抽象类替代 TypeScriptinterface作为 Schema 载体、用InterfaceType定义接口、用implements选项让对象类型及接口类型实现接口、借助类继承与接口继承构建Node → Person → Student这样的多层类型体系、在接口字段上声明参数与解析器、通过autoRegisterImplementations与orphanedTypes精确控制 Schema 注册范围以及通过返回类型类实例或自定义resolveType解决运行时类型探测问题。接口是抽象与复用 GraphQL 类型体系的核心工具合理运用上述机制可以在保持 Schema 类型安全的同时显著减少对象类型间的重复字段定义。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL 接口类型GraphQL Interface实战指南用抽象类与装饰器定义接口、实现继承与类型解析TypeGraphQL 接口类型GraphQL Interface实战指南用抽象类与装饰器定义接口、实现继承与类型解析 TypeGraphQL 的核心思想后端GraphQLAPI设计TypeGraphQL 接口类型InterfaceType完全指南基于抽象类定义 GraphQL Interface 及继承实现TypeGraphQL 接口类型InterfaceType完全指南基于抽象类定义 GraphQL Interface 及继承实现 TypeGraphQL后端GraphQLAPI设计TypeGraphQL 接口类型Interface Type完全指南用抽象类定义 GraphQL 接口TypeGraphQL 接口类型Interface Type完全指南用抽象类定义 GraphQL 接口 TypeGraphQL 的核心设计理念是 用 T后端GraphQLAPI设计上一篇为什么选择Enumify5大理由让你的代码更清晰高效下一篇Enumify核心功能解析enumKeys、enumValues与迭代器全攻略创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考