☰
使用 TypeGraphQL 定义 GraphQL Union 联合类型:从 createUnionType 到 resolveType 完整实战指南
2026/9/28 2:22:44 网站建设 项目流程
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

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

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

导读

联合类型(Union)让 GraphQL API 可以在一个字段中返回多种不同类型的对象,例如电影网站搜索接口同时返回Movie和Actor。本文以 TypeGraphQL 的createUnionType为核心,讲解如何在类与装饰器驱动的 TypeScript 项目中定义 Union 类型、在 Resolver 中返回对应实例,以及通过resolveType精确控制运行时类型判定,并辅以仓库源码与测试用例佐证其底层实现。

为什么需要 Union 类型

GraphQL 规范允许字段的返回类型是「一组可能类型中的一种」。以电影网站的搜索功能为例:用户输入关键词后,数据库里既能命中电影(Movie),也能命中演员(Actor)。此时查询结果无法用一个固定的 Object Type 表达,而需要返回Movie | Actor的集合。

在 GraphQL 中这正对应Union Type的定义:它本身不包含字段,只是罗列一组成员 Object Type,客户端必须用内联片段(inline fragment)按成员类型取字段。关于该类型的规范语义,可参考 官方 GraphQL 文档。

TypeGraphQL 提供了两个层面的支持:

  • 装饰器@ObjectType()用于定义成员类型类;
  • createUnionType工厂函数用于声明 Union 类型并注册到元数据存储中,供buildSchema生成 Schema。

定义 Union 的成员类型

先用装饰器定义两个成员类型。以文档中的电影搜索示例为基础:

@ObjectType() class Movie { @Field() name: string; @Field() rating: number; }
import { Int } from "type-graphql"; @ObjectType() class Actor { @Field() name: string; @Field(type => Int) age: number; }

两点需要注意:

  1. 每个成员类都必须带@ObjectType()装饰器,因为 Union 的成员必须是 Object Type;
  2. 数值字段如age需要用@Field(type => Int)显式指定标量类型,否则反射会将其推断为默认的Float。

用 createUnionType 创建联合类型

import { createUnionType } from "type-graphql"; const SearchResultUnion = createUnionType({ name: "SearchResult", // GraphQL Schema 中的联合类型名称 types: () => [Movie, Actor] as const, // 返回成员 Object Type 类元组的函数 });

配置项说明:

配置项类型说明
namestring必填,生成的 GraphQL Union 类型名,如SearchResult
types() => readonly ClassType[]必填,惰性返回成员类元组的函数
descriptionstring可选,Schema 中该 Union 类型的描述
resolveTypeTypeResolver可选,自定义运行时类型判定函数,见下文

从源码看,该工厂函数实现于 src/decorators/unions.ts:它接收name、description、types与resolveType,通过getMetadataStorage().collectUnionMetadata(...)将配置收集进元数据存储,并返回一个唯一的symbol作为该 Union 类型的标识。声明类型UnionTypeConfig<TClassTypes>还通过UnionFromClasses(定义在 src/helpers/utils.ts)把类的元组推导为InstanceType联合,实现编译期类型安全。

为什么types必须是函数、且要用as const

types被设计为函数而不是直接传数组,是为了惰性求值、避免循环依赖:Schema 生成时各类的元数据可能尚未收集完毕,因此只有真正构建 Union 时才调用该函数取出类列表。源码 src/schema/schema-generator.ts 中,typesThunk会在所有objectTypesInfo构建完成后被调用一次,并把结果映射为GraphQLUnionType的成员类型。

而as const语法把[Movie, Actor]标记为元组而不是普通数组,这样UnionFromClasses能精确推导出Movie | Actor类型,而不是Movie[] | Actor[]之类的宽泛类型,从而让typeof SearchResultUnion具有准确的编译期类型。

在 Resolver 中返回 Union 类型

定义好 Union 后,将其作为@Query的返回类型注解传入。注意:必须显式使用装饰器返回类型注解,因为 TypeScript 的类型反射(design:returntype)无法识别这种「类型变量」,这是 TypeScript 反射机制的固有限制。

@Resolver() class SearchResolver { @Query(returns => [SearchResultUnion]) async search(@Arg("phrase") phrase: string): Promise<Array<typeof SearchResultUnion>> { const movies = await Movies.findAll(phrase); const actors = await Actors.findAll(phrase); return [...movies, ...actors]; } }

这里的typeof SearchResultUnion在编译期等价于Movie | Actor,既保证了返回值类型安全,又与实际运行时返回的对象保持一致。若要返回 Union 列表,则写成returns => [SearchResultUnion];单个对象则为returns => SearchResultUnion。

从仓库示例 examples/enums-and-unions/search-result.union.ts 与 examples/enums-and-unions/resolver.ts 可以看到同款用法:SearchResult联合Recipe | Cook,search查询把食谱与厨师结果合并返回。

Resolving Type:运行时如何判定具体类型

默认行为:返回类实例

当查询/变更的返回类型(或字段类型)是 Union 时,Resolver 必须返回某个成员类的具体实例。默认情况下,graphql-js需要借助「实例」来识别底层 GraphQL 类型;如果直接返回普通 JS 对象(plain object),将无法判定类型。

该默认行为对应源码 src/schema/schema-generator.ts 中的兜底逻辑:未提供resolveType时,默认函数会用instance instanceof ObjectClassType在成员类中查找匹配项,再映射为对应类型名;若找不到则抛出UnionResolveTypeError。

自定义 resolveType:返回普通对象

更灵活的做法是在createUnionType配置中提供自己的resolveType实现。这样 Resolver 里可以返回普通 JS 对象,由resolveType根据数据对象的形状来判定类型:

const SearchResultUnion = createUnionType({ name: "SearchResult", types: () => [Movie, Actor] as const, // 根据数据形状检测返回的对象类型 resolveType: value => { if ("rating" in value) { return Movie; // 返回带 @ObjectType() 的成员类 } if ("age" in value) { return "Actor"; // 或直接返回类型在 Schema 中的名称字符串 } return undefined; }, });

resolveType的返回值有两种合法形式:

  • 成员类本身(Movie),TypeGraphQL 会将其映射到对应 GraphQL 类型;
  • Schema 中的类型名字符串("Actor"),直接作为 GraphQL 类型名返回。

配置类型定义见 src/decorators/types.ts 的ResolveTypeOptions:resolveType?: TypeResolver<TSource, TContext>,即一个接收数据源、返回类型名的函数。

测试用例 tests/functional/unions.ts 覆盖了这两种路径:

  • 第 51-62 行UnionWithStringResolveType用字符串返回类型名;
  • 第 65-76 行UnionWithClassResolveType返回成员类;
  • 对应第 226、246 行的用例分别验证「用字符串/类正确识别返回对象类型」;
  • 第 539 行起的用例还验证了resolveType返回undefined时 Schema 执行会报错:"Abstract type "OneTwo" must resolve to an Object type at runtime...",提示需要提供resolveType或isTypeOf。

客户端查询:使用内联片段取字段

定义并构建 Schema 后,客户端查询时需要为每个成员类型使用... on TypeName内联片段:

query { search(phrase: "Holmes") { ... on Actor { # Maybe Katie Holmes? name age } ... on Movie { # For sure Sherlock Holmes! name rating } } }

由于 Union 类型本身没有字段,客户端必须按成员类型分别取字段,name在两个片段中分别对应各自类型的name字段。

进阶用法与示例

更多关于 Union(以及 Enum)的进阶用法,可直接查看仓库中的完整可运行示例:

  • examples/enums-and-unions/index.ts 入口与 Schema 构建;
  • examples/enums-and-unions/search-result.union.ts Union 定义;
  • examples/enums-and-unions/resolver.ts 联合返回的查询实现;
  • examples/enums-and-unions/schema.graphql 生成的 Schema 文件,可对照查看union SearchResult = Cook | Recipe的最终形态。

小结

  • 使用@ObjectType()定义成员类型,再用createUnionType({ name, types })创建 Union;
  • types采用惰性函数 +as const元组,兼顾循环依赖规避与 TypeScript 类型推导;
  • Resolver 返回类型必须显式注解为 Union,运行时要么返回成员类实例(默认判定),要么通过resolveType按数据形状返回类或类型名字符串;
  • 客户端需用... on内联片段访问各成员字段。

通过以上方式,即可在 TypeGraphQL 项目中优雅地实现「一个查询返回多种类型」的灵活 API 设计。

  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

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

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载
上一篇:4个实用技巧解决RevokeMsgPatcher微信防撤回补丁失效问题
下一篇:国家中小学智慧教育平台电子课本解析工具:三步获取完整PDF教材的终极指南

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询