news 2026/9/28 6:28:43

TypeGraphQL 联合类型(Union Types)实战指南:用 `createUnionType` 构建多形态查询返回

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeGraphQL 联合类型(Union Types)实战指南:用 `createUnionType` 构建多形态查询返回
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

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

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

本篇技术指南聚焦 TypeGraphQL 中 GraphQL Union Type(联合类型)的完整实现方案:从定义@ObjectType类、通过createUnionType组装联合、到在@Query中使用并解决运行时的类型判定问题。读完本文,你将掌握如何让一个查询灵活返回多种对象类型(如电影站点同时返回Movie或Actor)、如何利用resolveType让 resolver 返回普通对象,以及底层 Schema 生成器与元数据存储的实现原理。

什么是 Union Type,为什么需要它

在真实 API 场景中,一个查询的返回值往往不是单一、固定的类型,而是"一组可能类型中的某一种"。比如电影网站的搜索功能:用户输入关键词后,数据库里既可能匹配到电影,也可能匹配到演员,因此搜索查询需要返回Movie | Actor这类结果。GraphQL 规范为此提供了Union Type(联合类型)——它表示"返回的类型是以下类型之一",客户端可以通过内联片段(inline fragment)按需选取字段。

在 TypeGraphQL 中,联合类型完全基于类与装饰器声明式构建,无需手写 SDL(Schema Definition Language),类型安全由 TypeScript 编译期保证。

第一步:定义作为联合成员的对象类型

联合类型的成员必须是@ObjectType()装饰的类。沿用上文电影搜索的例子,先创建两个对象类型:

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

注意两点:

  • 每个成员类必须用@ObjectType()标记,字段用@Field()标记,否则无法进入 Schema 生成流程;
  • @Field(type => Int)用于显式声明非推断的标量类型(如number对应的 GraphQLInt),确保生成的 SDL 准确。

仓库中的官方示例 examples/enums-and-unions/cook.type.ts 展示了同样的写法(Cook类使用@Field(_type => Int)声明yearsOfExperience)。

第二步:用createUnionType组装联合类型

TypeGraphQL 通过createUnionType函数将一组对象类型类声明为联合类型。这里用到了比较少见的[ ] as const语法——它的作用是告知 TypeScript 编译器这是一个元组(tuple),从而获得更精确的 TS 联合类型推断:

import { createUnionType } from "type-graphql"; const SearchResultUnion = createUnionType({ name: "SearchResult", // Name of the GraphQL union types: () => [Movie, Actor] as const, // function that returns tuple of object types classes });

配置对象的核心字段:

  • name:联合类型在 GraphQL Schema 中的名称(必填);
  • types:返回对象类型类元组的函数(必填)。之所以设计为函数(thunk)而非直接传数组,是为了解决循环引用问题——延迟到 Schema 构建阶段再解析具体类;
  • description:可选的描述文本,会写入生成的 GraphQL 描述;
  • resolveType:可选的自定义类型判定函数,详见下文"Resolving Type"小节。

从源码看,createUnionType的定义位于 src/decorators/unions.ts,其泛型签名UnionTypeConfig<TClassTypes extends readonly ClassType[]>保证了传入types的类数组类型安全,返回值为UnionFromClasses<T>工具类型。UnionFromClasses会把类元组映射为对应的 TS 联合类型(如Movie | Actor),因此后续typeof SearchResultUnion在编译期就等于Movie | Actor。

底层上,函数内部调用getMetadataStorage().collectUnionMetadata({ name, description, getClassTypes: types, resolveType })收集元数据,并返回一个以name命名的 Symbol 作为该联合类型的标识(见 src/metadata/metadata-storage.ts)。联合类型元数据的结构定义在 src/metadata/definitions/union-metadata.ts:包含getClassTypes、name、description?与resolveType?。

第三步:在 Query 中返回联合类型

创建好联合后,即可把它作为@Query的返回类型。注意必须显式使用装饰器的返回类型标注(returns => ...),因为 TypeScript 的反射(emitDecoratorMetadata)无法推断装饰器标注之外的泛型信息;同时为了编译期类型安全,方法返回类型应写为typeof SearchResultUnion:

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

这里[SearchResultUnion]表示返回的是联合类型的列表。官方示例 examples/enums-and-unions/resolver.ts 中search查询也采用了完全一致的写法:

@Query(_returns => [SearchResult]) async search(@Arg("cookName") cookName: string): Promise<Array<typeof SearchResult>> { const recipes = this.recipesData.filter(recipe => recipe.cook.name.match(cookName)); const cooks = this.cooks.filter(cook => cook.name.match(cookName)); return [...recipes, ...cooks]; }

联合类型同样可以出现在对象类型字段上:在@Field()中传入联合值即可,测试用例 tests/functional/unions.ts 演示了@Field(() => OneTwoThreeUnion)的用法。

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

这是联合类型使用中最容易踩坑的地方。当查询/变更的返回类型(或字段类型)是联合类型时,resolver 必须返回对象类型类的具体实例。如果返回的是普通 JS 对象(plain object),graphql-js将无法确定底层 GraphQL 类型。

默认行为:基于instanceof的判定

如果不提供resolveType,TypeGraphQL 在 Schema 生成时会使用默认判定逻辑:遍历联合的成员类,用instance instanceof ObjectClassType判断返回值属于哪个类,再映射到对应的 GraphQL 类型。从源码 src/schema/schema-generator.ts 可以看到,若遍历后找不到匹配的类,会抛出UnionResolveTypeError错误(定义于 src/errors/UnionResolveTypeError.ts),提示信息为:

Cannot resolve type for unionxxx! You need to return instance of object type class, not a plain object!

对应的测试用例 tests/functional/unions.ts 专门验证了这一行为:当 resolver 返回普通对象{ fieldTwo: "fieldTwo" }时,查询结果报错,错误消息包含 "resolve"、"instance"、"plain" 等关键词。

自定义resolveType:允许返回普通对象

如果希望 resolver 返回普通对象(例如直接返回 ORM 查询到的数据实体,而不手动new类实例),可以向createUnionType传入自定义的resolveType函数。此时由你根据数据对象的形状(shape)自行判定其类型:

const SearchResultUnion = createUnionType({ name: "SearchResult", types: () => [Movie, Actor] as const, // Implementation of detecting returned object type resolveType: value => { if ("rating" in value) { return Movie; // Return object type class (the one with `@ObjectType()`) } if ("age" in value) { return "Actor"; // Or the schema name of the type as a string } return undefined; }, });

resolveType的返回值支持两种形式(见 src/typings/TypeResolver.ts 中TypeResolver类型定义:MaybePromise<Maybe<string | ClassType>>):

  • 返回对象类型类本身:如return Movie,TypeGraphQL 会解析该类对应的 GraphQL 类型;
  • 返回 Schema 中的类型名字符串:如return "Actor",直接以字符串指定类型名。

两相对照,测试用例 tests/functional/unions.ts 分别构造了返回字符串(UnionWithStringResolveType)和返回类(UnionWithClassResolveType)的两种联合,并验证在 resolver 返回普通对象时都能正确解析出__typename。此外,测试还覆盖了resolveType返回undefined的边界情况——此时graphql-js会报出 "Abstract type ... must resolve to an Object type at runtime" 的标准错误(见 tests/functional/unions.ts)。

底层原理:Schema 生成器如何构建GraphQLUnionType

在 Schema 构建阶段(src/schema/schema-generator.ts),TypeGraphQL 遍历metadataStorage.unions,为每个联合创建一个GraphQLUnionType:

  • types通过 thunk 延迟求值:在对象类型全部构建完成后,再从objectTypesInfoMap中取出成员类的GraphQLObjectType;
  • 若配置了自定义resolveType,则包装为getResolveTypeFunction;
  • 否则使用默认的instanceof判定逻辑,找不到匹配类时抛出UnionResolveTypeError。

联合类型标识 Symbol 被记录在unionTypesInfoMap中,供字段/查询类型解析时查表(src/schema/schema-generator.ts)。

客户端查询示例

Schema 构建完成后,客户端可以这样发起查询,利用内联片段按类型取字段:

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

... on Actor与... on Movie是 GraphQL 标准的内联片段语法,客户端在拿到响应后可通过__typename区分具体类型。

进阶参考:完整示例与测试

  • 官方可运行示例:更进阶的联合类型(与枚举组合)用法见 examples/enums-and-unions 目录,其中 search-result.union.ts 定义了Recipe与Cook的联合SearchResult,resolver.ts 展示了完整的查询实现,schema.graphql为生成后的 SDL 参考;
  • 功能测试:联合类型的全部行为(默认 instanceof 判定、字符串/类形式的resolveType、普通对象报错、同一联合用于多 Schema 构建等)由 tests/functional/unions.ts 覆盖,可作为验证和理解行为边界的权威依据。

小结

在 TypeGraphQL 中使用联合类型只需四步:定义@ObjectType成员类 → 用createUnionType组装并传入types元组 → 在@Query的返回类型标注中引用联合值 → 确保 resolver 返回类实例,或提供自定义resolveType以便返回普通对象。掌握resolveType的类/字符串两种返回形式与默认instanceof判定机制,即可在实际项目中灵活设计"多形态返回"的查询接口。

  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

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

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

相关推荐

上一篇:F´ 遥测打包组件 Svc::TlmPacketizer 深入解析:基于哈希槽的分组遥测架构与调优指南
下一篇:7个Riko流处理引擎实战痛点解决方案:从入门到精通

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

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

济南网站建设与优化避坑指南:3步对比评测省50%预算

济南网站建设与优化避坑指南:3步对比评测省50%预算 找建站公司最怕什么?不是慢,是贵。济南不少老板找服务商,报价单上“源码交付”“SEO优化”写得花里胡哨,结果交完钱发现是个套壳模板,改个颜色都要加钱。这种被坑高价的情况,在济南网站建设市场太常见了。…

作者头像 李华
网站建设 2026/9/28 6:28:16

NOAA中国区域18类气象要素逐日数据整理(1942-2025)CSV全解析

打开 NOAA 的 FTP 目录找中国区域气象数据时&#xff0c;我最初的感受是“资料又多又乱”&#xff1a;几十年按年打包的 gz 文件、两套互相补充的数据集、一堆奇怪的编码字段。但真正把这套数据整理成一份能直接给模型用的 CSV 之后&#xff0c;我发现它几乎是免费可得、覆盖时…

作者头像 李华
网站建设 2026/9/28 6:27:49

帮建网站多少钱?别被坑了,这3点决定生死

帮建网站多少钱?别被坑了,这3点决定生死 网站上线三个月,后台数据一片死寂。每天盯着 Google Search Console 的曲线,心里直打鼓:这钱是不是白花?很多老板问我,帮建网站到底要多少钱,为什么有的报价几千,有的报价几万,效果却天差地别?…

作者头像 李华
网站建设 2026/9/28 6:27:48

5个坑教你搞定wordpressqq微博同步与建站报价

5个坑教你搞定wordpressqq微博同步与建站报价 模板网站太丑,改不动?很多老板拿着几千块拿到的模板站,发现不仅难看,还连不上微信微博,流量全靠天意。这时候问一句“建站报价多少”,往往得到的是一堆虚高数字,因为没人告诉你, wordpressqq微博…

作者头像 李华
网站建设 2026/9/28 6:27:46

网站地图wordpress从零搭建

WordPress网站地图搭建避坑指南:6个关键注意事项 网站被黑挂马不知道怎么办?别慌,这通常不是服务器问题,而是你的WordPress站点存在“死链”或结构混乱,给了黑客可乘之机。很多站长只盯着内容更新,却忽略了 网站地图(Sitemap)…

作者头像 李华
网站建设 2026/9/28 6:27:05

AI建站工具横评:产品经理如何用Framer、Durable、10Web、Bubble快速做落地页

先说个我上个月的真实经历。手头一个内部产品的Demo要赶在季度评审前上线&#xff0c;按老流程走&#xff0c;得找UI出图、前端切页面、后端接接口&#xff0c;最快也得排两周。那次我试着用AI建站工具自己搭&#xff0c;从描述需求到拿到一个能点击、能提交表单、能适配移动端…

作者头像 李华