TypeGraphQL 自定义装饰器(Custom Decorators)完整实战指南:方法、类与参数三种模式及底层原理

原创2026-09-27 23:59:021,455 阅读
文章标签:后端GraphQLAPI设计

TypeGraphQL 自定义装饰器(Custom Decorators)完整实战指南:方法、类与参数三种模式及底层原理

导读

在 TypeGraphQL 中,@Query、@Mutation、@Arg、@Ctx 等内置装饰器帮助我们用类与装饰器的方式声明式地构建 GraphQL schema 与 resolver。但当多个 resolver 之间存在重复的校验、鉴权、参数注入等逻辑时,就会产生大量样板代码。TypeGraphQL 提供了一整套自定义装饰器机制——包括方法装饰器(Method Decorators)、Resolver 类装饰器(Resolver Class Decorators)和参数装饰器(Parameter Decorators)——让我们可以把通用逻辑封装成语义化、可复用的装饰器,直接应用于字段、整个 Resolver 类或单个方法参数。读完本文,你将掌握三种自定义装饰器的创建方式、参数注入与 @Arg 注册的进阶用法,并通过仓库源码理解其底层实现原理。

为什么需要自定义装饰器

原文档指出,自定义装饰器是"减少样板代码、在 resolver 之间复用通用逻辑"的利器。TypeGraphQL 支持三种自定义装饰器:

类型 辅助函数 返回类型 作用范围
方法装饰器 createMethodMiddlewareDecorator MethodDecorator 单个字段/resolver 方法
Resolver 类装饰器 createResolverClassMiddlewareDecorator ClassDecorator 整个 Resolver 类(作用于该类下所有 Query/Mutation)
参数装饰器 createParameterDecorator ParameterDecorator 单个方法参数,可向方法注入返回值

这三种辅助函数均从 type-graphql 包导出,在仓库中位于 src/decorators 目录,对应源码文件为 createMethodMiddlewareDecorator.ts、createResolverClassMiddlewareDecorator.ts 与 createParameterDecorator.ts。

方法装饰器:用中间件逻辑封装语义化装饰器

方法装饰器本质上是中间件(middleware)的语法糖封装。TypeGraphQL 的中间件机制详见 middlewares.md,它允许复用 resolver 间的公共代码;而自定义方法装饰器在此基础上进一步收敛 API。

创建方式:调用 createMethodMiddlewareDecorator 辅助函数,传入中间件逻辑(MiddlewareFn)并返回其结果:

export function ValidateArgs(schema: JoiSchema) {
  return createMethodMiddlewareDecorator(async ({ args }, next) => {
    // 中间件代码,可使用自定义装饰器传入的参数
    // 例如基于 'joi' schema 的校验逻辑
    await joiValidate(schema, args);
    return next();
  });
}

从源码看,createMethodMiddlewareDecorator 的实现极为简洁——它直接返回内置的 @UseMiddleware 装饰器:

// src/decorators/createMethodMiddlewareDecorator.ts
export function createMethodMiddlewareDecorator<TContextType extends object = object>(
  resolver: MiddlewareFn<TContextType>,
): MethodDecorator {
  return UseMiddleware(resolver);
}

也就是说,自定义方法装饰器 = @UseMiddleware(中间件函数) 的再封装。中间件签名 MiddlewareFn<TContext> 接收两个参数:action: ResolverData<TContext>(包含 root、args、context、info)与 next: NextFn,定义见 src/typings/middleware.ts。

使用方法:将自定义装饰器放在 resolver/字段上方并传入所需参数,还可以与内置的 @UseMiddleware 混用:

@Resolver()
export class RecipeResolver {
  @ValidateArgs(MyArgsSchema) // 自定义装饰器
  @UseMiddleware(ResolveTime) // 显式中间件
  @Query()
  randomValue(@Args() { scale }: MyArgs): number {
    return Math.random() * scale;
  }
}

注意装饰器的执行顺序:TypeGraphQL 会按装饰器声明顺序(自上而下)收集并执行中间件,即 ValidateArgs 的校验先于 ResolveTime 的计时执行。

Resolver 类装饰器:一次声明,作用于整个类

与方法装饰器类似,我们可以创建作用于整个 Resolver 类的自定义装饰器。此时需要调用 createResolverClassMiddlewareDecorator:

export function ValidateArgs(schema: JoiSchema) {
  return createResolverClassMiddlewareDecorator(async ({ args }, next) => {
    // 中间件代码,可使用自定义装饰器传入的参数
    // 例如基于 'joi' schema 的校验逻辑
    await joiValidate(schema, args);
    return next();
  });
}

其用法与方法装饰器几乎一致,只是装饰器被放置于 Resolver 类之上:

@ValidateArgs(MyArgsSchema) // 自定义装饰器
@UseMiddleware(ResolveTime) // 显式中间件
@Resolver()
export class RecipeResolver {
  @Query()
  randomValue(@Args() { scale }: MyArgs): number {
    return Math.random() * scale;
  }
}

这样,我们只需在代码中放置一次,自定义装饰器就会被应用到该 Resolver 的所有 Query 与 Mutation 上。

底层原理同样清晰:createResolverClassMiddlewareDecorator 也只是对 UseMiddleware 的封装,唯一区别是返回类型为 ClassDecorator:

// src/decorators/createResolverClassMiddlewareDecorator.ts
export function createResolverClassMiddlewareDecorator<TContextType extends object = object>(
  resolver: MiddlewareFn<TContextType>,
): ClassDecorator {
  return UseMiddleware(resolver);
}

@UseMiddleware 本身是一个同时支持类与方法的重载装饰器(UseMiddleware.ts):

  • 当 propertyKey 为空(装饰在类上)时,调用 collectResolverMiddlewareMetadata 将该中间件注册到整个 Resolver 类;
  • 当装饰在方法上时,调用 collectMiddlewareMetadata 仅注册到该字段。

因此同一个 @UseMiddleware 才能既被方法装饰器复用,也被类装饰器复用。

参数装饰器:向方法注入返回值

参数装饰器与中间件最大的不同在于:它有能力返回一个值并注入到方法的对应参数中。这有效减少了原先通过污染 context 来在中间件与 resolver 之间通信的变通做法。

参数装饰器可以只是一个简单的数据提取函数,让 resolver 更易于单元测试:

function CurrentUser() {
  return createParameterDecorator<MyContextType>(({ context }) => context.currentUser);
}

也可以更高级,封装部分计算逻辑。相比中间件,参数装饰器对代码执行时机有更细粒度的控制——例如只在真正被请求时才基于 GraphQL info 计算字段映射(通过 @Fields() 装饰器触发):

function Fields(level = 1): ParameterDecorator {
  return createParameterDecorator(async ({ info }) => {
    const fieldsMap: FieldsMap = {};
    // 基于 GraphQL 解析器的 'info' 参数与 level 参数
    // 计算请求字段的对象信息
    // 甚至可以调用异步服务,因为它是普通 async 函数,可以直接 'await'
    return fieldsMap;
  });
}

性能提醒(原文档强调):自定义参数装饰器的逻辑若是 async 函数,可能会拖慢 GraphQL resolver 的执行速度,因此如无必要应尽量使用同步逻辑。

参数装饰器在 resolver 中的用法与内置装饰器(@Args、@Arg、@Ctx 等)完全一致,其返回值会直接作为对应参数的实参:

@Resolver()
export class RecipeResolver {
  constructor(private readonly recipesRepository: Repository<Recipe>) {}

  @Authorized()
  @Mutation(returns => Recipe)
  async addRecipe(
    @Args() recipeData: AddRecipeInput,
    // 自定义装饰器,用法与内置装饰器一致
    @CurrentUser() currentUser: User,
  ) {
    const recipe: Recipe = {
      ...recipeData,
      // 在 resolver 代码中使用自定义装饰器返回的数据
      author: currentUser,
    };
    await this.recipesRepository.save(recipe);

    return recipe;
  }

  @Query(returns => Recipe, { nullable: true })
  async recipe(
    @Arg("id") id: string,
    // 从 GraphQL 查询 info 中解析字段的自定义装饰器
    @Fields() fields: FieldsMap,
  ) {
    return await this.recipesRepository.find(id, {
      // 将字段映射作为 select 投影以优化数据库查询
      select: fields,
    });
  }
}

从源码层面看,createParameterDecorator 返回的装饰器在调用时会将元数据收集到 metadata storage:

// src/decorators/createParameterDecorator.ts
getMetadataStorage().collectHandlerParamMetadata({
  kind: "custom",
  target: prototype.constructor,
  methodName: propertyKey,
  index: parameterIndex,
  resolver,
  options,
});

其中 kind: "custom" 对应的元数据结构为 CustomParamMetadata(见 src/metadata/definitions/param-metadata.ts),它保存了 resolver 函数与可选参数注册选项。最终在 schema 生成阶段,src/resolvers/create.ts 的 createHandlerResolver 中会先执行中间件链,再由 getParams 依次解析各参数的元数据、调用自定义参数 resolver 获取值,最后通过 targetInstance[methodName].apply(targetInstance, params) 调用 resolver 方法——这正是"参数注入"得以实现的底层执行链路。

自定义 @Arg 装饰器:注册并暴露 GraphQL 参数

某些场景下,我们希望自定义装饰器不仅能注入参数值,还能在 GraphQL schema 中注册/暴露一个参数。此时如果在一个自定义装饰器内同时调用 Arg() 与 createParameterDecorator(),会与 TypeGraphQL 内部机制产生冲突。

为此,createParameterDecorator() 支持第二个参数 CustomParameterOptions,其中 arg 键用于提供 @Arg 的元数据,实现"一举两得":

function RandomIdArg(argName = "id") {
  return createParameterDecorator(
    // 在此实现"取用户提供的参数,或生成随机 id"的逻辑
    ({ args }) => args[argName] ?? Math.round(Math.random() * MAX_ID_VALUE),
    {
      // 在此提供元数据,将参数注册为 GraphQL 参数
      arg: {
        name: argName,
        typeFunc: () => Int,
        options: {
          nullable: true,
          description: "Accepts provided id or generates a random one.",
        },
      },
    },
  );
}

CustomParameterOptions 的类型定义(arg 选项包含 name、typeFunc 与可选的 options: ArgOptions)见 src/decorators/createParameterDecorator.ts,其中 ArgOptions 合并了类型选项、描述、校验与废弃标记等配置(定义见 src/decorators/Arg.ts)。从源码可以看到,当传入 paramOptions.arg 时,createParameterDecorator 内部会调用与 @Arg 相同的 getParamInfo 辅助函数(src/helpers/params.ts),通过 design:paramtypes 反射元数据或 typeFunc 推断 GraphQL 类型,并把这些信息一并存入 options.arg——这正是自定义 @Arg 装饰器能正确生成 schema 参数的原因。

使用时与普通的 @Arg 装饰器几乎无差别:

@Resolver()
export class RecipeResolver {
  constructor(private readonly recipesRepository: Repository<Recipe>) {}

  @Query(returns => Recipe, { nullable: true })
  async recipe(
    // 自定义装饰器:在 schema 中暴露一个 arg
    @RandomIdArg("id") id: number,
  ) {
    return await this.recipesRepository.findById(id);
  }
}

小提示:arg.options 同样支持传入 validateFn 自定义校验函数。仓库示例 random-id-arg.ts 中就对生成/传入的 id 做了取值范围校验:validateFn 会在值超出 [0, MAX_ID_VALUE] 时抛出错误。

仓库实战示例:middlewares-custom-decorators

原文档末尾提供了配套示例项目。在仓库中,完整的可运行示例位于 examples/middlewares-custom-decorators,包含三种自定义装饰器的真实实现:

  • decorators/validate-args.ts:基于 class-validator 的 ValidateArgs 方法装饰器。它接收一个 ClassType<T>,将 args 实例化后调用 validate(),若存在校验错误则抛出 ArgumentValidationError,否则继续执行 next()。注释明确指出:示例使用 class-validator,你也可以换成 joi 或任意校验库。
  • decorators/current-user.ts:极简参数装饰器,从 context 中提取 currentUser,泛型参数显式标注为 Context 类型。
  • decorators/random-id-arg.ts:自定义 @Arg 装饰器的完整实现,包含 arg 元数据与 validateFn 校验。
  • decorators/index.ts:统一导出上述装饰器。

在 recipe/recipe.resolver.ts 中可以看到它们的组合用法:类级使用 @UseMiddleware(ResolveTimeMiddleware),recipes 查询上叠加 @ValidateArgs(RecipesArgs)(参数类见 recipe.args.ts,包含 @Min(0) 的 skip 与 @Min(1) @Max(50) 的 take),并同时使用 @RandomIdArg("id") 与 @CurrentUser() 两个参数装饰器。运行示例项目即可直观观察装饰器对查询入参校验、执行耗时统计与上下文注入的实际效果。

总结

自定义装饰器是 TypeGraphQL 将"可复用逻辑"与"声明式 API"结合的精华特性:

  1. 方法装饰器(createMethodMiddlewareDecorator)把中间件逻辑封装成语义化装饰器,应用于单个字段;
  2. Resolver 类装饰器(createResolverClassMiddlewareDecorator)把同样的逻辑提升到类级别,一次声明、作用于该类所有 Query/Mutation;
  3. 参数装饰器(createParameterDecorator)直接向方法参数注入返回值,比中间件更精细地控制执行时机,且天然友好于单元测试;
  4. 自定义 @Arg 装饰器借助 CustomParameterOptions 的 arg 键,在注入参数值的同时把参数注册进 GraphQL schema,替代"内部同时调用 Arg() 与 createParameterDecorator()"的冲突写法。

从源码看,方法/类装饰器本质都是 @UseMiddleware 的封装(createMethodMiddlewareDecorator.ts、createResolverClassMiddlewareDecorator.ts),而参数装饰器则通过 metadata storage 记录 kind: "custom" 的参数元数据,由 src/resolvers/create.ts 在 resolver 执行前统一解析注入。掌握这一机制后,你可以将校验、鉴权、字段投影、随机参数生成等通用逻辑沉淀为项目内部的"专属 DSL",让 resolver 代码保持干净、可读且易于测试。

登录后查看全文
type-graphql