- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
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的完整执行流程:
- 拒绝 symbol 类型的属性键(抛出
SymbolKeysNotSupportedError); - 解析重载参数(可能传入类型函数、options 对象,或两者都不传);
- 调用 findType.ts 读取反射元数据(
design:type或design:returntype),配合显式传入的类型函数确定最终 GraphQL 类型; - 调用
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在食谱还没有评分时是未定义的),我们需要两处配合:
- 在 TypeScript 侧用
?:把属性声明为可选; - 在
@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/implements | src/decorators/ObjectType.ts |
@Field | 声明属性为 GraphQL 字段,收集反射元数据,支持nullable/description/deprecationReason/name/complexity | src/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!
相关推荐
TypeGraphQL 类型与字段映射全指南:用 TypeScript 类与装饰器构建 GraphQL Schema
TypeGraphQL 类型与字段映射全指南:用 TypeScript 类与装饰器构建 GraphQL Schema 导读 本指南聚焦 TypeGraphQL
后端GraphQLAPI设计RustOwl开发路线图:未来版本功能预测与展望
RustOwl开发路线图:未来版本功能预测与展望 你是否在调试Rust程序时,仍为所有权和生命周期问题感到困惑?是否希望有更直观的工具帮助理解复杂的内存管理逻辑
后端GraphQLAPI设计TypeGraphQL 入门指南:用 TypeScript 类与装饰器声明式构建 GraphQL Schema 与 Resolver
TypeGraphQL 入门指南:用 TypeScript 类与装饰器声明式构建 GraphQL Schema 与 Resolver 导读 TypeGraphQ
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考