- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
本篇技术指南聚焦 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 union
xxx! 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!
相关推荐
TypeGraphQL 中的联合类型(Union Types):用 createUnionType 构建灵活的多类型查询返回
TypeGraphQL 中的联合类型(Union Types):用 createUnionType 构建灵活的多类型查询返回 GraphQL 的联合类型(Uni
后端GraphQLAPI设计type-graphql 联合类型(Union Types)实战:用 createUnionType 构建可返回多类型结果的 GraphQL 查询
type graphql 联合类型(Union Types)实战:用 createUnionType 构建可返回多类型结果的 GraphQL 查询 本篇指南围绕
后端GraphQLAPI设计type-graphql Union 类型实战指南:用 createUnionType 构建灵活的多类型查询返回
type graphql Union 类型实战指南:用 createUnionType 构建灵活的多类型查询返回 在 GraphQL 服务开发中,接口有时必须返
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考