news 2026/9/28 2:21:41

TypeGraphQL 接口类型(Interface)完整指南:用 TypeScript 类定义 GraphQL Interface

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeGraphQL 接口类型(Interface)完整指南:用 TypeScript 类定义 GraphQL Interface
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载

导读

本指南以 TypeGraphQL 的@InterfaceType()装饰器为核心,系统讲解如何在 TypeScript 中定义 GraphQL 接口类型、让对象类型实现接口、接口继承接口,以及如何为接口字段编写 resolver。读完本文,你将掌握接口类型从定义、实现、注册到运行时类型解析(resolveType)的完整链路,并能正确处理 Relay 风格Node接口在多个 schema 中按需暴露等进阶场景。全文示例均可在仓库 examples/interfaces-inheritance 与 tests/functional/interfaces-and-inheritance.ts 中找到可运行佐证。

为什么用抽象类来表示接口

TypeGraphQL 的核心思想是:基于 TypeScript 类来创建 GraphQL 类型。GraphQL 规范本身提供了 Interface Type(接口类型),用于描述多个对象类型必须共同遵守的字段契约,面向对象编程中也会用接口来约束实现类。因此 TypeGraphQL 原生支持定义 GraphQL 接口。

但 TypeScript 的interface是纯编译期构造,运行时不产生任何实体,无法承载装饰器元数据,也就无法在运行时构建 GraphQL schema。解决方案是使用抽象类(abstract class)模拟接口:它同样不能被实例化、可以被其他类实现,与interface的唯一差别是它不会在编译期强制校验方法实现或字段初始化——只要开发者自觉把抽象类当作接口对待,就可以安全使用。

定义接口类型

创建 GraphQL 接口与创建对象类型几乎一致:定义一个抽象类并用@InterfaceType()装饰,字段形状仍由@Field声明:

@InterfaceType() abstract class IPerson { @Field(type => ID) id: string; @Field() name: string; @Field(type => Int) age: number; }

生成的 GraphQL SDL 为:

interface IPerson { id: ID! name: String! age: Int! }

从源码看,@InterfaceType装饰器最终调用getMetadataStorage().collectInterfaceMetadata(...)把接口元数据(名称、目标类、实现的接口列表、resolveType、autoRegisteringDisabled等)存入元数据存储,见 src/decorators/InterfaceType.ts 与 src/metadata/definitions/interface-class-metadata.ts。接口类型与其他类型一样由MetadataStorage.build()统一构建元数据(src/metadata/metadata-storage.ts 中的buildClassMetadata(this.interfaceTypes))。

用对象类型实现接口

定义好接口后,让对象类型实现它:

@ObjectType({ implements: IPerson }) class Person implements IPerson { id: string; name: string; age: number; }

与普通对象类型唯一的区别是:必须在@ObjectType装饰器中通过implements选项告知 TypeGraphQL 该类实现了哪个接口。实现多个接口时传入数组:

@ObjectType({ implements: [IPerson, IAnimal, IMachine] }) class Hybrid implements IPerson, IAnimal, IMachine { // ... }

implements选项在源码中被定义在 src/decorators/types.ts 的ImplementsClassOptions接口中(implements?: Function | Function[]),@ObjectType会将其转为数组存入对象类型元数据(src/decorators/ObjectType.ts)。当 schema 生成器为对象类型构建interfaces时,会逐一在interfaceTypesInfoMap中查找接口类,找不到会抛出 "Cannot find interface type metadata" 错误(src/schema/schema-generator.ts 的interfaces回调)。

字段装饰器可以省略:接口中声明的字段会被自动复制到实现该接口的对象类型上(下文"字段如何合并"会从源码层验证),因此Person类里无需重复写@Field,只需依赖 TypeScript 自身的类型检查来保证接口实现正确,避免维护两份定义。

接口抽象类也可以被继承:对象类型可以extends接口抽象类,基类的字段会被继承并在 schema 中一并输出:

@ObjectType({ implements: IPerson }) class Person extends IPerson { @Field() hasKids: boolean; }

schema 生成器在处理对象类型时,会通过Object.getPrototypeOf找到父类并复制父类字段(src/schema/schema-generator.ts 中 "support for extending interface classes - get field info from prototype" 的逻辑)。

字段如何合并到实现类

在 schema 生成阶段(src/schema/schema-generator.ts 的fields回调)中可以看到合并细节:先把实现类implements的接口元数据中的字段全部 push 进fieldsMetadata,再 push 对象类型自身的字段,利用数组顺序让自身字段覆盖继承字段,随后统一转换为GraphQLFieldConfigMap。这也解释了为什么接口字段可以在实现类中省略装饰器——它们已被完整搬运。

接口实现接口(graphql-js 15.0+)

自graphql-js15.0 起,接口类型也可以实现其他接口类型。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; }

当对象类型实现了一个"已经实现了其他接口"的接口时,@ObjectType的implements数组只需列出继承链上最近的那个接口,无需全部列出:

@ObjectType({ implements: [Person] }) class Student extends Person { @Field() universityName: string; }

上述三段代码在 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 implements Node & Person:Node是被自动补全的。源码中对象类型构建interfaces时,会先映射显式列出的接口类,再检查对象类型是否继承了父类,若父类是接口/对象类型则把父类的interfaces合并进来并去重(Array.from(new Set(...)),见 src/schema/schema-generator.ts)。

接口字段的 Resolver 与参数

接口上的字段可以定义 resolver,语法与对象类型字段完全相同:

@InterfaceType() abstract class IPerson { @Field() firstName: string; @Field() lastName: string; @Field() fullName(): string { return `${this.firstName} ${this.lastName}`; } }

这些 resolver 会被所有实现该接口的对象类型继承——前提是对象类型自己没有为同名字段提供实现。

带参数的接口字段:若想声明类似avatar(size: Int!): String!的接口字段,直接在方法参数上使用@Arg或@Args即可:

@InterfaceType() abstract class IPerson { @Field() avatar(@Arg("size") size: number): string { return `http://i.pravatar.cc/${size}`; } }

生成的接口 SDL 为:

interface IPerson { avatar(size: Int!): String! }

抽象方法的两难:TypeScript 不允许在抽象方法上使用装饰器,所以如果只想约束方法签名(参数与返回类型)而不提供实现,必须在方法体内抛出错误占位:

@InterfaceType() abstract class IPerson { @Field() avatar(@Arg("size") size: number): string { throw new Error("Method not implemented!"); } }

随后所有实现该接口的对象类型都必须继承并覆写此方法:

@ObjectType({ implements: IPerson }) class Person extends IPerson { avatar(size: number): string { return `http://i.pravatar.cc/${size}`; } }

扩展签名:如果实现类想为字段追加额外参数(如format),必须整体重声明完整字段签名,无法只追加参数:

@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}`; } }

在 Resolver 类中定义接口字段 resolver:也可以使用@FieldResolver与@Root,作用目标为接口类:

@Resolver(of => IPerson) class IPersonResolver { @FieldResolver() avatar(@Root() person: IPerson, @Arg("size") size: number): string { return `http://typegraphql.com/${person.id}/${size}`; } }

该模式在 examples/interfaces-inheritance/person/person.interface.ts 中有完整体现:IPerson接口声明了带size参数的avatar字段并用throw new Error("Method not implemented.")占位,Person类型(examples/interfaces-inheritance/person/person.type.ts)则继承并覆写了具体实现,生成的 examples/interfaces-inheritance/schema.graphql 中可以看到avatar(size: Float!): String!已正确出现在接口与各实现类型上。

接口在 schema 中的注册与 autoRegisterImplementations

默认行为:只要接口被显式用于 schema 定义(作为 query/mutation 的返回类型,或某个字段的类型),所有实现该接口的对象类型都会自动注册进 schema,无需额外配置。

何时需要关闭自动注册:以 Relay 体系的Node接口为例,当同一套类型要暴露多个互相隔离的 schema(如公开 schema 与私有 schema)时,默认的全量自动注册可能不合预期。此时可传入{ autoRegisterImplementations: false }:

@InterfaceType({ autoRegisterImplementations: false }) abstract class Node { @Field(type => ID) id: string; }

关闭后,需要把希望在该 schema 中暴露的实现类显式加入buildSchema的orphanedTypes数组:

const schema = await buildSchema({ resolvers, // Provide orphaned object types orphanedTypes: [Person, Animal, Recipe], });

相关源码链路如下:

  • @InterfaceType将autoRegisterImplementations === false映射为元数据字段autoRegisteringDisabled(src/decorators/InterfaceType.ts);
  • schema 生成器在buildOtherTypes中自动收集"被使用接口的实现类"时,会先检查implementedInterfaceInfo.metadata.autoRegisteringDisabled,若为true则跳过自动注册(src/schema/schema-generator.ts 第 617-632 行);
  • 而usedInterfaceTypes集合只在类型真正作为输出类型被引用时才会被写入(同文件第 865 行的this.usedInterfaceTypes.add(interfaceType.target));
  • orphanedTypes数组中的类(对象/接口/输入类型)会被filterTypesInfoByOrphanedTypesAndExtractType过滤并强制提取进最终 schema(src/schema/schema-generator.ts 第 946-951 行)。

注意:如果某个对象类型类被显式用作 GraphQL 类型(例如Recipe作为addRecipemutation 的返回类型),那么无论orphanedTypes如何设置,它都会被注册进 schema。

仓库测试 tests/functional/interfaces-and-inheritance.ts 第 1454-1506 行专门验证了这一行为:@InterfaceType({ autoRegisterImplementations: false })的接口在只提供orphanedTypes: [FirstSampleObject]时,FirstSampleObject出现在 schema 中,而未列出的SecondSampleObject不会出现。

运行时类型解析(resolveType)

默认要求:当对象类型实现 GraphQL 接口时,resolver 中必须返回类型类的真实实例(如Object.assign(new Person(), ...)构造出的实例),否则graphql-js无法正确判定底层 GraphQL 类型。schema 生成器默认的resolveType实现正是通过instance instanceof typeCls逐类匹配来识别类型(src/schema/schema-generator.ts 第 497-504 行,匹配失败会抛出InterfaceResolveTypeError,见 src/errors/InterfaceResolveTypeError.ts)。

自定义 resolveType:可以在@InterfaceType选项中提供自己的resolveType函数,从而允许 resolver 返回普通对象(plain object),运行时通过数据形状判断具体类型——这与 union 类型的做法一致:

@InterfaceType({ resolveType: value => { if ("grades" in value) { return "Student"; // 返回类型在 schema 中的名称字符串 } return Person; // 或返回对象类型类 }, }) abstract class IPerson { // ... }

resolveType的返回值可以是类型名字符串,也可以是对象类型类本身。源码中getResolveTypeFunction会对返回值做归一化:字符串直接返回,类则通过possibleObjectTypesInfo.find(objectType => objectType.target === resolvedType)映射为其 schema 名称(src/schema/schema-generator.ts 第 925-936 行)。此外,传入的resolveType支持返回 Promise(异步判定)。

需要提醒的是:与 union 相比,接口场景下自定义resolveType会更棘手,因为实现同一接口的对象类型可能很多,你未必能全部记住。仓库示例 examples/interfaces-inheritance/person/person.interface.ts 采用了resolveType: value => value.constructor.name的通用兜底方案(该文件注释中还标注了这是 issue #373 的 workaround)。在 examples/interfaces-inheritance/resolver.ts 中可以看到配套做法:personsquery 返回IPerson[]接口数组,新增学生/员工时通过Object.assign(new Student(), ...)构造真实类实例,从而保证类型可被正确解析。

完整示例:查询返回接口类型

仓库的 examples/interfaces-inheritance 提供了接口+继承的完整可运行示例,核心结构包括:

  • person/person.interface.ts:定义IPerson接口(id、name、age字段 + 带size参数的avatar占位实现);
  • person/person.type.ts、student/student.type.ts、employee/employee.type.ts:分别实现IPerson并扩展各自字段;
  • resolver.ts:personsquery 直接返回[IPerson]数组,addStudent/addEmployeemutation 写入真实实例;
  • schema.graphql:最终生成的 SDL,展示了interface IPerson、type Person/Student/Employee implements IPerson以及带参数的avatar(size: Float!): String!字段;
  • 对应的输入类型见 person/person.input.ts、student/student.input.ts、employee/employee.input.ts。

功能测试位于 tests/functional/interfaces-and-inheritance.ts,覆盖了接口实现、多接口实现、接口继承接口、字段合并覆盖、implements传非接口类型时报错、autoRegisterImplementations与orphanedTypes等场景,可作为行为契约参考。

小结

在 TypeGraphQL 中定义 GraphQL 接口的要点可归纳为四条:用抽象类 +@InterfaceType()声明接口形状;用implements选项让对象类型(或接口)实现它,字段装饰器可省略、可继承父类;接口字段的 resolver 用方法体、@Arg/@Args或@FieldResolver定义;运行时要保证 resolver 返回类型类实例,或为接口提供自定义resolveType。面对多 schema 隔离需求时,再用autoRegisterImplementations: false与orphanedTypes精确控制暴露范围。

  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载
上一篇:Kinto同步功能详解:实现多设备数据无缝同步的终极指南
下一篇:3个维度解锁Windows隐藏能力:ViVeTool深度探索指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 2:19:30

S7-1500模拟量处理全解析:NORM_X与SCALE_X指令详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:18:35

【Excel经验】字符串处理方法

文章目录 前言 一、概览-公式汇总 1 把多列内容拼接在一起,作为新的一列的内容 2 某列的内容是数值,但是格式是字符串形式,需要转换为数值类型,方便计算 3 截取指定位置的子串 3.1 左截取LEFT、LEFTB 3.2 右截取RIGHT、RIGHTB 3.3 中间截取 MID MIDB 4 分割字符串用得到后的…

作者头像 李华