news 2026/9/27 8:48:22

TypeGraphQL 类型与字段:用类与装饰器声明 GraphQL Object Type

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeGraphQL 类型与字段:用类与装饰器声明 GraphQL Object Type
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

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

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

TypeGraphQL 的核心思路,是从 TypeScript 类自动生成 GraphQL schema 定义,无需手写 SDL 文件或重复描述 schema 的接口。本篇指南以 Recipe 模型为例,完整讲解@ObjectType与@Field装饰器的用法:如何声明字段、显式标注数组与泛型类型、精确控制列表及嵌套列表的 nullability、为字段添加描述与弃用标记,以及如何在 schema 中重命名类型与字段。读完本文,你将能独立用 TypeGraphQL 定义出严谨、可读、与graphql-js语义完全一致的对象类型。

从 TypeScript 类到 GraphQL 类型:核心思想

TypeGraphQL 的设计目标非常明确:以类与装饰器为唯一的事实来源,自动生成 GraphQL schema 定义。这避免了传统方案中"SDL 文件 + 接口/类型 + 解析器"三处重复维护的痛点,仅凭装饰器和一点点 TypeScript 反射(reflection)机制即可完成 schema 生成。

先从一个普通的 TypeScript 类开始。它代表我们的Recipe数据模型,包含存储食谱数据的字段:

class Recipe { id: string; title: string; ratings: Rate[]; averageRating?: number; }

此时它只是一个普通类,GraphQL 并不知道它的存在。要让 TypeGraphQL 把它当作 GraphQL 的type(即 SDL 中的type关键字,或graphql-js中的GraphQLObjectType),第一步是给类加上@ObjectType()装饰器:

@ObjectType() class Recipe { id: string; title: string; ratings: Rate[]; averageRating: number; }

@ObjectType装饰器内部会把类的元数据收集到 TypeGraphQL 的 metadata storage 中——从 ObjectType.ts 源码可以看到,它调用了getMetadataStorage().collectObjectMetadata(...)并注册name(默认取target.name)、description、implements(接口实现)等信息。

不过,仅仅标记类还不够。类里的哪些属性要暴露为 GraphQL 字段,需要逐个声明——这正是@Field装饰器的职责。

用 @Field 声明属性并收集反射元数据

@Field装饰器做两件事:声明类属性映射为 GraphQL 字段,同时从 TypeScript 反射系统收集该属性的类型元数据。我们给Recipe的每个公开属性都加上@Field():

@ObjectType() class Recipe { @Field() id: string; @Field() title: string; @Field() ratings: Rate[]; @Field() averageRating: number; }

从 Field.ts 源码可以看到@Field的完整执行流程:

  1. 拒绝 symbol 类型的属性键(抛出SymbolKeysNotSupportedError);
  2. 解析重载参数(可能传入类型函数、options 对象,或两者都不传);
  3. 调用 findType.ts 读取反射元数据(design:type或design:returntype),配合显式传入的类型函数确定最终 GraphQL 类型;
  4. 调用collectClassFieldMetadata注册字段元数据,包括schemaName(即 options.name 或属性名)、getType、typeOptions、complexity、description、deprecationReason等。

对于string、boolean、number这类简单类型,@Field()什么都不传就够了——反射系统能直接读出正确类型。真正需要显式标注的是泛型类型。

数组类型必须显式声明:type => [T]

由于 TypeScript 反射系统的限制,装饰器只能拿到属性声明处的构造器(如Array),拿不到泛型参数(如Rate)。所以声明Rate[]时,必须用@Field(type => [Rate])的显式数组语法告诉编译器:

@Field(type => [Rate]) ratings: Rate[];

嵌套数组同样用[ ]符号逐层标注深度。例如@Field(type => [[Int]])表示期望一个深度为 2 的整数数组。

为什么这里采用函数语法而不是{ type: Rate }这样的配置对象?因为函数(thunk)语法能规避循环依赖问题(例如Post <--> User相互引用时,模块加载顺序导致的undefined引用)。这也是社区普遍接受这一约定(convention)的原因。如果你想少敲几个键,可以写成简写@Field(() => Rate),但可读性略差,需要读者自行权衡。

从实现层面看,findType.ts 中的findTypeValueArrayDepth函数会递归展开returnTypeFunc()的返回值,解析出数组深度(arrayDepth)与最内层元素类型,供 schema 生成阶段构造GraphQLList。在 types.ts 中ReturnTypeFunc被定义为(returns?: void) => TypeValue | RecursiveArray<TypeValue>,RecursiveArray正是允许任意嵌套数组字面量的类型来源。

覆盖反射推断:type => ID / Int / 自定义标量

@Field的类型函数同样可以覆盖反射推断出的类型。例如Recipe.id属性在 TypeScript 中是string,但 GraphQL 语义上我们希望它是ID标量:

@Field(type => ID) id: string;

Rate.value是number,但我们希望映射为整数标量Int:

@Field(type => Int) value: number;

ID、Int是 TypeGraphQL 提供的三个基础标量别名(Int→GraphQLInt、Float→GraphQLFloat、ID→GraphQLID),用于省去引入graphql包的键盘开销。注意 JavaScript 的Number类型默认会映射为GraphQLFloat(见 helpers/types.ts 中convertTypeIfScalar的case Number: return GraphQLFloat),因此number属性想映射成Int时必须显式传type => Int。关于这些标量的完整说明(包括内置的Date标量与自定义标量注册)参见 scalars.md。

隐藏字段:不写 @Field 的属性不进 schema

Rate类中还有一个微妙的细节——user属性没有@Field()装饰器:

@ObjectType() class Rate { @Field(type => Int) value: number; @Field() date: Date; user: User; // 没有 @Field,不暴露到 schema }

这正是一种数据隐藏手段:user字段需要持久化到数据库(例如用于防止同一用户重复评分),但你不希望它通过 GraphQL 公之于众。只对类中需要公开的属性加@Field()即可。

上面Rate类生成的 SDL 等价物如下——user没有出现在其中:

type Rate { value: Int! date: Date! }

nullability:默认非空,按需放宽

TypeGraphQL 的默认行为与 TypeScript 属性语义保持一致:所有字段默认非空(non-null)。也就是@Field()默认生成String!、Int!这样的类型。

单值字段的 nullable: true

当属性可能没有值(比如averageRating在食谱还没有评分时是未定义的),我们需要两处配合:

  1. 在 TypeScript 侧用?:把属性声明为可选;
  2. 在@Field配置中传入{ nullable: true }。
@Field({ nullable: true }) averageRating?: number;

⚠️ 特别注意:当你把类型声明为可空联合(如string | null)时,必须显式给@Field提供类型函数——因为 TypeScript 对联合类型的反射结果通常是Object,无法自动推断出正确的 GraphQL 类型。这一点在 scalars.md 的示例中也有印证(get optionalInfo(): string | undefined时需要显式@Field(type => String, { nullable: true }))。

列表字段的精细化空值控制

列表类型的 nullability 比单值更复杂,因为"列表整体"和"列表元素"的空值语义是相互独立的。{ nullable: true | false }这种基础配置只作用于列表整体,对应 SDL 中的[Item!](列表可空)或[Item!]!(列表非空)。如果需要一个稀疏数组(元素允许为 null),则要使用两个特殊取值:

nullable 取值生成 SDL语义
false(默认)[Item!]!列表非空、元素非空
true[Item!]列表可空、元素非空
"items"[Item]!列表非空、元素可空
"itemsAndList"[Item]列表可空、元素也可空

注意:nullableByDefault: true(在buildSchema配置中开启,见 bootstrap.md)同样会作用于列表,把默认的[Item!]!变成[Item]——效果等同于nullable: "itemsAndList"。

嵌套列表的空值传播规则

对于嵌套列表,nullable选项会作用于整个数组深度。以@Field(() => [[Item]])为例:

  • 默认情况:生成[[Item!]!]!(每一层都非空);
  • nullable: "itemsAndList":生成[[Item]](每一层都可空);
  • nullable: "items":生成[[Item]]!(最外层非空,内层可空)。

这些规则在源码层面有精确对应。helpers/types.ts 的wrapWithTypeOptions函数是整个空值包装逻辑的核心:它根据typeOptions.nullable与nullableByDefault判断每一层是否用GraphQLNonNull包裹;wrapTypeInNestedList则递归按arrayDepth构造嵌套的GraphQLList。同时,把"items"/"itemsAndList"用在非数组字段上会直接抛出WrongNullableListOptionError(见 errors/index.ts),因此这两个选项只适用于列表字段。

测试用例也对上述空值规则做了完整覆盖:在 tests/functional/fields.ts 中,arrayWithNullableItemField(nullable: "itemsAndList")验证生成"列表可空、元素可空"的结构,nonNullArrayWithNullableItemField(nullable: "items")验证"列表非空、元素可空",而nonNullNestedArrayWithNullableItemField与nestedArrayWithNullableItemField则分别验证嵌套数组在"items"与"itemsAndList"下的逐层类型结构。

description 与 deprecationReason:让 schema 自文档化

在@Field的配置对象中,还可以提供面向 GraphQL schema 用途的元信息:

  • description:字段描述,会写入 schema 并出现在 GraphQL introspection 与文档工具中;
  • deprecationReason:字段弃用原因,标注后客户端工具会在 schema 中把该字段标记为@deprecated。

@ObjectType同样支持description(ObjectTypeOptions类型中description与implements等选项的定义见 ObjectType.ts)。字段元数据在 field-metadata.ts 中对应description与deprecationReason两个属性,均来自@Field的 options。

完整示例与生成的 SDL

把前面所有特性组合起来,Recipe类最终长这样:

@ObjectType({ description: "The recipe model" }) class Recipe { @Field(type => ID) id: string; @Field({ description: "The title of the recipe" }) title: string; @Field(type => [Rate]) ratings: Rate[]; @Field({ nullable: true }) averageRating?: number; }

这段声明生成的 GraphQL schema 片段(SDL)为:

type Recipe { id: ID! title: String! ratings: [Rate!]! averageRating: Float }

对照可见三条关键映射:string+type => ID→ID!;string+ description →String!(描述随 introspection 可见);Rate[]+type => [Rate]→[Rate!]!;number+{ nullable: true }→Float(可空)。

计算型字段与 field resolver

如果对象类型的某个字段纯粹由其他字段计算而来(例如averageRating由ratings数组算得),而且你不想污染类的签名,可以完全省略该属性,转而通过 field resolver 实现。field resolver 的详细做法(@FieldResolver()+@Root()注入父对象、ResolverInterface<T>增强类型安全等)参见 resolvers.md,其中也包含averageRating的完整实现示例。

注意事项与边界

禁止定义构造函数

在对象类型类中定义构造函数是严格禁止的——TypeGraphQL 在底层会自行创建对象类型类的实例(相关机制可参考 helpers/types.ts 中convertToType函数,它通过new (Target as any)()来实例化输入数据对应的类型)。自定义构造函数会破坏这一实例化流程。

用 name 重命名类型与字段

某些场景下,我们希望内部类名/属性名与对外暴露的 schema 名称不同。@ObjectType与@Field都支持name:

@ObjectType("ExternalTypeName") class InternalClassName { @Field({ name: "externalFieldName" }) internalPropertyName: string; }

在 ObjectType.ts 中,getNameDecoratorParams会解析第一个字符串参数作为name,最终注册的name: name || target.name;在 Field.ts 中,schemaName取options.name || propertyKey,且 metadata storage 同时保存name(内部属性名)与schemaName(对外 schema 名)。

⚠️ 但需注意:字段重命名只对输出类型(object type、interface type)有效,对输入类型(input type)无效。原因在于:输入字段没有 resolver 可以把一个字段值翻译成另一个属性值——重命名后没有"翻译层"来还原数据,因此 TypeGraphQL 不支持在输入侧做字段改名。

小结

主题关键点仓库依据
@ObjectType把类标记为 GraphQL object type,支持name/description/implementssrc/decorators/ObjectType.ts
@Field声明属性为 GraphQL 字段,收集反射元数据,支持nullable/description/deprecationReason/name/complexitysrc/decorators/Field.ts
数组与嵌套数组必须显式type => [T]/[[T]],函数语法解决循环依赖src/helpers/findType.ts
标量覆盖type => ID、type => Int覆盖反射推断docs/scalars.md
列表空值nullable: "items"/"itemsAndList"精细化控制元素与整体src/helpers/types.ts、tests/functional/fields.ts
全局默认空值buildSchema({ nullableByDefault: true })src/schema/build-context.ts

如果想在真实项目里看到这些字段定义方式的综合应用,可以直接阅读仓库中的示例代码,例如 examples/simple-usage 下的recipe.type.ts(Recipe对象类型定义)与recipe.input.ts(AddRecipeInput输入类型),以及 examples/generic-types/paginated-response.type.ts(用类工厂模式实现泛型分页类型,其中就包含@Field(type => [TItemClass])的数组类型声明)。更进一步,泛型类型的完整指南见 generic-types.md,接口与继承相关的字段扩展见 interfaces.md 与 inheritance.md。

</output文章>

  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

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

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载
上一篇:AG-UI路由管理终极指南:Next.js App Router最佳实践
下一篇:gorush源码贡献指南:从Issue到PR的完整流程

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

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

notepad-- 使用教程:双栏文件对比、整库批量替换与主题定制

notepad-- 使用教程&#xff1a;双栏文件对比、整库批量替换与主题定制 【免费下载链接】notepad-- 一个支持windows/linux/mac的文本编辑器&#xff0c;目标是做中国人自己的编辑器&#xff0c;来自中国。 项目地址: https://gitcode.com/GitHub_Trending/no/notepad-- …

作者头像 李华