- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
导读
联合类型(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; }两点需要注意:
- 每个成员类都必须带
@ObjectType()装饰器,因为 Union 的成员必须是 Object Type; - 数值字段如
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 类元组的函数 });配置项说明:
| 配置项 | 类型 | 说明 |
|---|---|---|
name | string | 必填,生成的 GraphQL Union 类型名,如SearchResult |
types | () => readonly ClassType[] | 必填,惰性返回成员类元组的函数 |
description | string | 可选,Schema 中该 Union 类型的描述 |
resolveType | TypeResolver | 可选,自定义运行时类型判定函数,见下文 |
从源码看,该工厂函数实现于 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!
相关推荐
type-graphql Unions 联合类型完全指南:从 `createUnionType` 定义到 `resolveType` 解析实战
type graphql Unions 联合类型完全指南:从 createUnionType 定义到 resolveType 解析实战 本指南以 type gr
后端GraphQLAPI设计type-graphql 联合类型(Union)完整实战指南:从 createUnionType 到类型解析
type graphql 联合类型(Union)完整实战指南:从 createUnionType 到类型解析 当 GraphQL API 需要让同一个查询字段返
后端GraphQLAPI设计TypeGraphQL Unions 实战指南:使用 `createUnionType` 定义与解析 GraphQL 联合类型
TypeGraphQL Unions 实战指南:使用 createUnionType 定义与解析 GraphQL 联合类型 本篇技术指南聚焦 TypeGraph
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考