拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TypeGraphQL 类型与字段详解:用装饰器与反射把 TypeScript 类自动映射为 GraphQL Schema

TypeGraphQL 类型与字段详解:用装饰器与反射把 TypeScript 类自动映射为 GraphQL Schema 后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载TypeGraphQL 的核心设计理念是从 TypeScript 类自动生成 GraphQL Schema 定义从而省去手写 SDLSchema Definition Language文件和重复的 schema 描述接口。本文以对象类型Recipe为贯穿示例系统讲解ObjectType与Field的用法、类型反射的边界、列表与可空性的精确控制、隐藏字段、重命名字段等关键技能并结合仓库源码src/decorators/Field.ts、src/helpers/findType.ts、src/helpers/types.ts等说明底层实现帮助你彻底掌握 TypeGraphQL 的类型声明体系。一、从普通 TypeScript 类到 GraphQL 类型的三个步骤TypeGraphQL 的思路很直接把类即类型的直觉变成现实。首先定义一个普通的 TypeScript 类代表领域模型Recipe字段用于存放菜谱数据class Recipe { id: string; title: string; ratings: Rate[]; averageRating?: number; }要让这个类成为 GraphQL 的type对应 SDL 中的type关键字或graphql-js中的GraphQLObjectType需要完成三步装饰第一步用ObjectType()标记类。它将该类注册为对象类型。在源码中ObjectType装饰器src/decorators/ObjectType.ts会把类名可通过name参数覆盖、可选描述和implements的接口列表写入全局元数据存储MetadataStorage.collectObjectMetadata见 src/metadata/metadata-storage.tsObjectType() class Recipe { id: string; title: string; ratings: Rate[]; averageRating: number; }第二步用Field()声明需要映射为 GraphQL 字段的属性。只有带Field()的属性才会进入 schemaField同时负责从 TypeScript 反射系统design:type元数据收集类型信息ObjectType() class Recipe { Field() id: string; Field() title: string; Field() ratings: Rate[]; Field() averageRating: number; }第三步为简单类型之外的字段显式标注类型。对于string、boolean这类简单类型反射足够可靠但受 TypeScript 反射机制限制Array、Promise等泛型类型无法被正确反射必须显式声明详见下一节。二、为什么泛型类型必须显式声明反射的边界与Field(type ...)语法TypeScript 的emitDecoratorMetadata只会把属性的构造函数引用写入design:type元数据无法保留ArrayRate中Rate这个泛型实参。在 src/helpers/findType.ts 中可以看到完整的解析逻辑Field装饰器先通过Reflect.getMetadata(design:type, prototype, propertyKey)取得反射类型若用户同时提供了returnTypeFunc即type ...函数则优先调用该函数获得真实类型并递归解析数组深度Field(type [Rate]) ratings: Rate[];这里推荐使用显式的[ ]语法声明数组。在findType的实现中当返回函数的结果是数组时会通过findTypeValueArrayDepth递归展开嵌套数组计算出array true与arrayDepthsrc/helpers/findType.ts例如Field(type [[Int]])会得到一个深度为 2 的整数二维数组。为什么是函数语法而不是{ type: Rate }对象因为函数是惰性求值的——只有真正生成 schema 时才会调用type Rate取回类引用。这解决了循环依赖问题如Post -- User互相引用时直接传对象会在模块加载阶段就引用未定义的类因此被确立为 TypeGraphQL 的约定。为了少敲几个字符你也可以用简写Field(() Rate)但显式写出类型对协作者可读性更好。反射不足时的兜底错误如果既没有显式提供类型函数反射到的又是被禁止的裸类型bannedTypes即String、Boolean、Number、Date、Array、Promise这类无法直接确定 GraphQL 类型的构造函数见 src/helpers/returnTypes.tsfindType会抛出NoExplicitTypeErrorsrc/errors/NoExplicitTypeError.ts提示开发者必须显式标注类型——这是避免生成歧义 schema 的重要防线。三、可空性nullable?:运算符与{ nullable: true }的配合默认情况下TypeGraphQL 的字段全部非空这与 TypeScript 属性默认非空的直觉一致对应 SDL 中的!标记。如果你希望全局放宽这一默认行为可以在buildSchema配置中设置nullableByDefault: true详见 docs/bootstrap.md。对于可能没有值的属性比如菜谱尚无评分时的averageRating需要两处配合用 TypeScript 的?:运算符把属性标为可选给Field传入{ nullable: true }。Field({ nullable: true }) averageRating?: number;需要特别留意当声明类型是可空联合如string | null时反射同样无法确定真实类型必须显式给Field提供类型否则无法正确生成 schema。底层如何包装可空类型在 src/helpers/types.ts 的wrapWithTypeOptions中非空字段会被new GraphQLNonNull(type)包装可空字段保持原样。同时要注意一个约束nullable: items或nullable: itemsAndList这类列表级选项只能用在数组字段上如果用在非数组字段会抛出WrongNullableListOptionErrorsrc/errors/WrongNullableListOptionError.ts。四、列表字段的精确可空性控制items与itemsAndList基础的可空性选项{ nullable: true | false }只作用于整个列表生成[Item!]或[Item!]!。但实际业务中常常需要稀疏数组——列表本身存在但里面的某些元素可能是null。此时可以通过nullable的两个特殊字符串值精确控制配置值生成的 SDL语义{ nullable: true }[Item!]列表整体可空元素不可空{ nullable: false }默认[Item!]!列表与元素都不可空{ nullable: items }[Item]!列表不可空但元素可空{ nullable: itemsAndList }[Item]列表与元素都可空在wrapWithTypeOptions的实现中nullable: items、nullable: itemsAndList以及在nullableByDefault: true且未显式设置nullable时都会让数组元素变成可空随后只有nullable: false、默认非空或nullable: items三种情形才会给整个列表套上GraphQLNonNullsrc/helpers/types.ts。这与文档描述的 SDL 输出完全吻合。对于嵌套列表这些选项作用于数组的每一层深度Field(() [[Item]])默认生成[[Item!]!]!设置nullable: itemsAndList会生成[[Item]]设置nullable: items则生成[[Item]]!。实现上由wrapTypeInNestedList递归按深度逐层包装src/helpers/types.ts。五、description与deprecationReason让 schema 自带文档Field的配置对象还支持为字段添加文档描述和废弃标记ObjectType同样支持description这些信息会原样进入生成的 GraphQL schema供 IDE 自动补全、工具链与下游消费者阅读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; }上面的类会生成如下 SDLtype Recipe { id: ID! title: String! ratings: [Rate!]! averageRating: Float }从源码看FieldOptions继承自AdvancedOptions除description、deprecationReason外还包含name字段重命名、complexity查询复杂度等选项src/decorators/types.ts它们都会在 src/decorators/Field.ts 中被收集为FieldMetadata完整结构见 src/metadata/definitions/field-metadata.ts最终由 SchemaGenerator 转成GraphQLObjectType的字段配置。六、覆盖反射类型ID、Int与内置Date标量Field(type ID)之类的写法不仅用来声明数组还能覆盖反射推断出的类型。例如把string类型的id显式声明为 GraphQL 的ID标量把number类型的value声明为Int而不是默认的FloatObjectType() class Rate { Field(type Int) value: number; Field() date: Date; user: User; }生成的 SDL 如下type Rate { value: Int! date: Date! }可以看到两点数值默认映射为FloatNumber对应GraphQLFloat见 src/helpers/types.ts 的convertTypeIfScalar因此需要Int时必须显式声明Date类型是内置支持的默认映射为GraphQLISODateTime同一函数第 44-45 行无需额外配置。更多关于ID、Int标量以及内置Date标量的细节参见 docs/scalars.md。七、用不加Field隐藏内部字段Field是白名单机制——没有装饰的属性不会出现在 GraphQL schema 中。上面Rate类中的user属性就是一个典型场景我们需要在数据库中保存user信息以防止同一位用户重复评分但不想把它公开给所有 API 消费者于是省略Field()字段就被隐藏了。八、可计算字段与字段解析器field resolver如果某个对象类型字段纯粹由其他字段计算得出例如由ratings数组算出的averageRating且你不想让它污染类的字段签名可以不在类中声明该属性而是用FieldResolver单独实现让解析逻辑与数据模型解耦详见 docs/resolvers.md。仓库的 examples/simple-usage/recipe.type.ts 展示了这一思路的实战写法specification与averageRating都是get访问器前者用{ nullable: true, deprecationReason: Use description field instead }演示了废弃字段声明后者用{ nullable: true }处理尚无评分的边界情况而ratings则用Field(_type [Int])显式声明整数数组——与本文介绍的配置项一一对应。九、禁止定义构造函数需要特别强调在对象类型类中定义构造函数是严格禁止的。TypeGraphQL 会在底层自行创建对象类型类的实例实例化后由字段解析器或默认属性读取填充数据因此不要在类中写自己的构造函数以免破坏实例创建流程。十、重命名让内部类名/属性名与外部 Schema 名解耦有时我们希望对外暴露的类型名或字段名与内部类名、属性名不同。TypeGraphQL 通过装饰器的name参数/属性实现重命名ObjectType(ExternalTypeName) class InternalClassName { Field({ name: externalFieldName }) internalPropertyName: string; }ObjectType(ExternalTypeName)把类名改为对外展示的类型名对应ObjectType的重载签名ObjectType(name, options?)见 src/decorators/ObjectType.tsField({ name: externalFieldName })把属性名改为 schema 中的字段名FieldMetadata.schemaName取options.name || propertyKey见 src/decorators/Field.ts。使用限制字段重命名仅适用于输出类型对象类型、接口类型等。原因在于输出类型有对应的解析器field resolver负责把externalFieldName的值翻译回internalPropertyName而输入类型的字段没有解析器可以完成这种值映射因此不能对输入字段重命名。结语ObjectTypeField是 TypeGraphQL 一切能力的地基从类到 SDL 的自动推导、数组深度解析、可空性的四档控制、标量覆盖、字段隐藏与重命名再到可计算字段的解析器化全部围绕这套声明式体系展开。理解了 src/decorators/Field.ts、src/helpers/findType.ts 与 src/helpers/types.ts 中的反射解析与类型包装逻辑你就能预判哪些写法会生成怎样的 schema从而写出类型安全、schema 精确的 GraphQL 服务。下一站可以继续阅读 docs/resolvers.md 学习字段解析器或通过 examples/simple-usage 目录中的可运行示例加深理解。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL 类型与字段映射全指南用 TypeScript 类与装饰器构建 GraphQL SchemaTypeGraphQL 类型与字段映射全指南用 TypeScript 类与装饰器构建 GraphQL Schema 导读 本指南聚焦 TypeGraphQL后端GraphQLAPI设计TypeGraphQL 类型与字段用类与装饰器声明 GraphQL Object TypeTypeGraphQL 类型与字段用类与装饰器声明 GraphQL Object Type TypeGraphQL 的核心思路是从 TypeScript 类后端GraphQLAPI设计TypeGraphQL 入门用 TypeScript 类与装饰器构建 GraphQL Schema 与 ResolverTypeGraphQL 入门用 TypeScript 类与装饰器构建 GraphQL Schema 与 Resolver TypeGraphQL 是一个面向后端GraphQLAPI设计上一篇PEFT FRoD 详解基于旋转自由度的全秩高效微调方法下一篇如何只安装Garden Skills中的单个技能-s参数全解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表