ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

TypeGraphQL Resolvers 完全指南:用类与方法编写 Query、Mutation 与字段解析器

2026/9/28 8:51:14 拓冰建站 浏览量
TypeGraphQL Resolvers 完全指南:用类与方法编写 Query、Mutation 与字段解析器 后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载TypeGraphQL 允许开发者像编写传统 REST 控制器如 Java Spring、.NET Web API、TypeScript routing-controllers一样用普通的类方法声明 GraphQL 的查询Query、变更Mutation与字段解析器Field Resolver从而把 GraphQL 层与业务代码干净地分离。本文基于 TypeGraphQL 官方文档website/versioned_docs/version-2.0.0-beta.3/resolvers.md并结合仓库源码系统讲解 Resolver 类的搭建、参数注入、Input 类型、字段解析器以及继承等实战要点读完你即可独立编写一套类型安全、可测试的 TypeGraphQL Resolver。一、Resolver 类GraphQL 世界里的控制器在声明完 对象类型Object Types 之后下一步就是创建 Resolver 类。只需给普通类打上Resolver()装饰器它就会像经典 REST 框架中的控制器一样工作Resolver() class RecipeResolver {}Resolver 类在应用中保证是单例single instance per app因此你可以在类内部保存数据也可以借助 DI 框架注入 Service 或 Repository 等依赖具体见 依赖注入文档Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; }从源码看Resolver()装饰器在 src/decorators/Resolver.ts 中实现它接受一个可选的类型函数TypeFunc并调用collectResolverClassMetadata把 Resolver 类的元数据收集到全局 metadata storage 中供后续 schema 生成器使用。第一个 Query声明返回类型接下来在类中添加处理 Query 的方法。以返回所有食谱的recipes查询为例Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; async recipes() { // Fake async return await this.recipesCollection; } }这里必须做两件事添加Query装饰器把该方法标记为 GraphQL 查询提供返回类型。由于方法是async的反射元数据系统reflection metadata会把返回类型识别为Promise所以需要用装饰器参数returns [Recipe]明确声明它解析为一个Recipe对象类型数组Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; Query(returns [Recipe]) async recipes() { return await this.recipesCollection; } }从源码可以看到Query装饰器在 src/decorators/Query.ts 中支持三种重载形式无参、仅 options、returnTypeFunc options最终通过getResolverMetadata解析出返回类型并调用collectQueryHandlerMetadata收集到 metadata storage 中。Mutation的实现完全对称见 src/decorators/Mutation.ts。二、Query 参数两种声明方式查询通常都带有参数——可能是资源 id、搜索关键词或分页设置。TypeGraphQL 提供两种定义参数的方式。方式一内联Arg()装饰器直接用Arg()装饰器声明单个参数。受反射系统限制需要在装饰器参数中重复参数名同时可以传入defaultValue选项该默认值会直接反映到 GraphQL schema 中Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes( Arg(servings, { defaultValue: 2 }) servings: number, Arg(title, { nullable: true }) title?: string, ): PromiseRecipe[] { // ... } }Arg的实现见 src/decorators/Arg.ts它收集kind: arg的 handler 参数元数据同时支持description、deprecationReason以及类型函数参数。当参数只有 23 个时这种方式很简洁但参数一多方法签名就会变得臃肿。方式二ArgsType()参数类参数较多时可以用一个类来描述整组参数。它看起来像对象类型类但顶部使用ArgsType()装饰器ArgsType() class GetRecipesArgs { Field(type Int, { nullable: true }) skip?: number; Field(type Int, { nullable: true }) take?: number; Field({ nullable: true }) title?: string; }ArgsType()在 src/decorators/ArgsType.ts 中实现调用collectArgsMetadata收集 Args 元数据Args()参数装饰器则在 src/decorators/Args.ts 中把整个参数类注入到方法。可选字段的默认值有两种设置途径在Field()装饰器中用defaultValue选项或用属性初始化器property initializer。两种方式 TypeGraphQL 都会在 schema 中体现为默认值客户端可以省略这些参数注意defaultValue只对输入型参数和字段生效即Arg、ArgsType和InputType。它不会影响ObjectType或InterfaceType的字段因为后者只用于输出。在 tests/functional/resolvers.ts 中可以看到对应的测试用例defaultValue会生成带默认值的非空参数类型如defaultValue: defaultStringArgDefaultValue并同时覆盖Arg与 input 对象字段两种场景。参数校验与辅助成员用参数类声明参数后还能顺势进行校验详见 validation 文档。参数类中也可以定义辅助字段和方法但严格禁止定义构造函数——TypeGraphQL 会在内部自行创建 Args 和 Input 类的实例import { Min, Max } from class-validator; ArgsType() class GetRecipesArgs { Field(type Int, { defaultValue: 0 }) Min(0) skip: number; Field(type Int) Min(1) Max(50) take 25; Field({ nullable: true }) title?: string; // Helpers - index calculations get startIndex(): number { return this.skip; } get endIndex(): number { return this.skip this.take; } }然后在方法中使用该参数类作为参数类型。还可以用解构语法直接取用单个参数变量而不必引用整个 args 对象Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes(Args() { title, startIndex, endIndex }: GetRecipesArgs) { // Example implementation let recipes this.recipesCollection; if (title) { recipes recipes.filter(recipe recipe.title title); } return recipes.slice(startIndex, endIndex); } }最终生成的 schema SDL 如下skip与take的默认值清晰地出现在参数定义中type Query { recipes(skip: Int 0, take: Int 25, title: String): [Recipe!] }三、Input 类型与 MutationMutation 的创建方式和 Query 基本一致声明类方法、使用Mutation装饰器、创建参数、按需提供返回类型。区别在于 Mutation 通常使用 input 类型TypeGraphQL 允许像创建对象类型一样通过InputType()装饰器创建输入类型InputType() class AddRecipeInput {}为了避免意外改变属性类型可以利用 TypeScript 类型检查系统实现Partial类型InputType() class AddRecipeInput implements PartialRecipe {}随后用Field()装饰器声明需要的输入字段InputType({ description: New recipe data }) class AddRecipeInput implements PartialRecipe { Field() title: string; Field({ nullable: true }) description?: string; }InputType的实现见 src/decorators/InputType.ts它支持传入自定义名称或DescriptionOptions如description并调用collectInputMetadata收集元数据。之后就可以在 Mutation 中使用AddRecipeInput类型了——可以内联使用Arg()装饰器也可以像上面的 Query 示例那样作为 args 类的字段。如果还需要访问上下文用Ctx()装饰器配合可选的自定义Context接口Resolver() class RecipeResolver { // ... Mutation() addRecipe(Arg(data) newRecipeData: AddRecipeInput, Ctx() ctx: Context): Recipe { // Example implementation const recipe RecipesUtils.create(newRecipeData, ctx.user); this.recipesCollection.push(recipe); return recipe; } }因为方法是同步的且显式返回Recipe所以可以省略Mutation()的类型标注。Ctx装饰器src/decorators/Ctx.ts甚至支持Ctx(propertyName)的形式直接取上下文的某个属性。上述声明生成的 schema SDLinput AddRecipeInput { title: String! description: String }type Mutation { addRecipe(data: AddRecipeInput!): Recipe! }借助参数装饰器我们不再需要处理root这类多余参数旧代码中通常用_前缀忽略既减少了方法定义的噪音又实现了 GraphQL 与业务代码的清晰分层——Resolver 及其方法就像普通 Service 一样可以轻松进行单元测试。四、Field Resolver解析对象类型的字段Query 和 Mutation 并非唯一的 Resolver 类型。当user类型带有posts字段、需要从数据库拉取关联数据时就需要对象类型的字段解析器。字段解析器与 Query/Mutation 非常相似同样是 Resolver 类上的方法但有几处不同。首先用Resolver装饰器的参数声明要解析的对象类型Resolver(of Recipe) class RecipeResolver { // Queries and mutations }然后在类中创建一个将作为字段解析器的方法。以Recipe对象类型中的averageRating字段为例它需要根据ratings数组计算平均值Resolver(of Recipe) class RecipeResolver { // Queries and mutations averageRating(recipe: Recipe) { // ... } }接着用FieldResolver()装饰器标记该方法。由于字段类型已在Recipe类定义中声明过这里无需重复声明同时用Root()装饰器注入 recipe 对象Resolver(of Recipe) class RecipeResolver { // Queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { // ... } }FieldResolver在 src/decorators/FieldResolver.ts 中实现它会尝试从design:returntype反射元数据中推断返回类型并收集kind: external的字段解析器元数据同时支持name、description、deprecationReason、complexity等选项。Root装饰器src/decorators/Root.ts同样支持Root(propertyName)形式直接注入根对象的指定属性。用ResolverInterface增强类型安全为了增强类型安全可以让 Resolver 类实现ResolverInterfaceRecipe接口。这是一个小型辅助接口用于校验字段解析器方法如averageRating(...)的返回类型是否与Recipe类中averageRating属性的类型匹配以及方法的第一个参数是否为实际的对象类型Recipe类Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { // Queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { // ... } }其类型定义位于 src/typings/ResolverInterface.ts{ [P in keyof T]?: (root: T, ...args: any[]) T[P] | PromiseT[P]; }也就是说每个方法必须接收T类型的根对象为第一参数返回类型必须与对应字段一致允许 Promise。下面是示例averageRating字段解析器的完整实现Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { // Queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { const ratingsSum recipe.ratings.reduce((a, b) a b, 0); return recipe.ratings.length ? ratingsSum / recipe.ratings.length : null; } }内联字段解析器 vs Resolver 类方法对于averageRating这类简单解析器或行为类似别名alias的已废弃字段可以直接在对象类型类定义中内联创建字段解析器ObjectType() class Recipe { Field() title: string; Field({ deprecationReason: Use title instead }) get name(): string { return this.title; } Field(type [Rate]) ratings: Rate[]; Field(type Float, { nullable: true }) averageRating(Arg(since) sinceDate: Date): number | null { const ratings this.ratings.filter(rate rate.date sinceDate); if (!ratings.length) return null; const ratingsSum ratings.reduce((a, b) a b, 0); return ratingsSum / ratings.length; } }但如果逻辑更复杂、包含副作用如 API 调用、数据库查询则应改用 Resolver 类方法。这样可以利用依赖注入机制显著提升可测试性。例如通过构造器注入 TypeORM 的Repositoryimport { Repository } from typeorm; Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { constructor( // Dependency injection private readonly userRepository: RepositoryUser, ) {} FieldResolver() async author(Root() recipe: Recipe) { const author await this.userRepository.findById(recipe.userId); if (!author) throw new SomethingWentWrongError(); return author; } }值得注意如果字段解析器的字段名在 Resolver 的对象类型中并不存在TypeGraphQL 会在 schema 中自动创建一个同名的新字段。这一特性非常适合纯粹的可计算字段如根据ratings数组计算出的averageRating避免污染类签名。五、Resolver 继承与进阶示例Resolver 类的继承属于进阶主题详见 resolver inheritance 文档。继承允许你复用父 Resolver 中的查询、变更与字段解析器方法同时保持子类各自的类型目标是大型项目中降低样板代码的有效手段。本文中的代码示例均为教程用途而构造更完整、贴近真实项目的例子可以参考仓库中的 examples 目录其中包含 simple-usage、resolvers-inheritance、middlewares-custom-decorators 等可直接运行的完整示例覆盖了从基础 Resolver 到中间件、依赖注入等进阶组合场景。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL Resolvers 完全指南用 TypeScript 类与方法构建 Query、Mutation 与字段解析器TypeGraphQL Resolvers 完全指南用 TypeScript 类与方法构建 Query、Mutation 与字段解析器 TypeGraphQL后端GraphQLAPI设计如何快速集成Material Menu到你的Android应用5分钟快速开始教程如何快速集成Material Menu到你的Android应用5分钟快速开始教程 想要为你的Android应用添加流畅的Material Design动画图标后端GraphQLAPI设计TypeGraphQL Resolvers 实战指南用 TypeScript 类与装饰器编写 Query、Mutation 与 Field ResolversTypeGraphQL Resolvers 实战指南用 TypeScript 类与装饰器编写 Query、Mutation 与 Field Resolvers后端GraphQLAPI设计上一篇axum 中间件完全指南基于 tower 的中间件架构、执行顺序与错误处理实战下一篇Errores encontrados创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考