- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
导读
在 TypeGraphQL 中,标量(Scalar)是连接 TypeScript 类型与 GraphQL 内置基础类型的关键一环。本指南基于 0.16.0 版本文档,系统讲解三类标量处理方案:用Int/Float/ID别名简化字段声明、内置 Date 标量的两种序列化格式,以及如何创建并接入自定义GraphQLScalarType(如 MongoDB 的 ObjectId)。读完本文,你将掌握字段类型自动推断的边界、通过buildSchema选项统一控制标量映射的方法,并能用scalarsMap让自定义标量"零注解"接入 schema。
一、基础标量别名:用Int、Float、ID少敲键盘
1.1 三个官方别名
TypeGraphQL 为 3 个基础标量提供了等价别名,定义在 src/scalars/aliases.ts 中,本质就是graphql包标量对象的直接引用:
Int→GraphQLIntFloat→GraphQLFloatID→GraphQLID
因此你可以用更短的写法在字段装饰器中声明类型:
import { ID, Float, Int } from "type-graphql"; @ObjectType() class MysteryObject { @Field(type => ID) readonly id: string; @Field(type => Int) notificationsCount: number; @Field(type => Float) probability: number; }1.2 自动推断的边界:哪些可以不写type =>
并非所有字段都必须显式写类型。TypeGraphQL 在 schema 生成阶段会执行一套"标量转换"逻辑,核心实现在 src/helpers/types.ts 的convertTypeIfScalar函数中(src/helpers/types.ts#L28-L49):
Number自动映射为GraphQLFloatString自动映射为GraphQLStringBoolean自动映射为GraphQLBooleanDate自动映射为GraphQLISODateTime(详见下文)
所以上面示例中的probability字段可以省略type => Float,直接写成:
@ObjectType() class MysteryObject { @Field() probability: number; }同理,GraphQLString与GraphQLBoolean不需要别名,能自动反射时直接留空即可:
@ObjectType() class User { @Field() name: string; @Field() isOld: boolean; }这一行为在测试 tests/functional/scalars.ts#L152-L178 中有明确验证:当属性类型为number时,内省结果(introspection)中字段类型是Float;属性类型为string时字段类型是String。
1.3 何时必须显式声明 String / Boolean
TypeScript 的emitDecoratorMetadata反射机制只能正确记录属性类型,而方法的返回类型在反射时会退化为Object。例如下面这个带 getter 的字段,TS 反射出的类型是Object,TypeGraphQL 无法推断它应映射到String,因此必须显式用 JS 构造函数String声明:
@ObjectType() class SampleObject { @Field(type => String, { nullable: true }) get optionalInfo(): string | undefined { // TS reflected type is `Object` :( if (Math.random() > 0.5) { return "Gotcha!"; } } }遇到这类反射信息缺失的情况,就用 JS 构造函数String、Boolean、Number作为显式类型兜底(测试 tests/functional/scalars.ts#L58-L65 中explicitStringField、explicitBooleanField正是这种用法)。
二、内置 Date 标量:ISO 格式与时间戳格式
2.1 两种格式与对应导出
TypeGraphQL 为Date类型内置了两种标量实现:
- 时间戳格式(
"timestamp"):序列化为毫秒级数字,如1518037458374 - ISO 格式(
"isoDate"):序列化为 ISO 8601 字符串,如"2018-02-07T21:04:39.573Z"
在 0.16.0 版本中,它们从type-graphql包导出为GraphQLISODateScalar与GraphQLTimestampScalar。需要说明的是,在后续版本(即当前仓库版本)中这两个标量的命名已演进为GraphQLISODateTime与GraphQLTimestamp,且实现来自graphql-scalarsnpm 包,可在 src/scalars/index.ts 中看到:
export { GraphQLTimestamp, GraphQLDateTimeISO as GraphQLISODateTime } from "graphql-scalars";2.2 默认格式与全局切换
默认情况下 TypeGraphQL 使用ISO 日期格式。0.16.0 文档中通过buildSchema的dateScalarMode选项切换格式:
import { buildSchema } from "type-graphql"; const schema = await buildSchema({ resolvers, dateScalarMode: "timestamp", // "timestamp" or "isoDate" });而在当前仓库版本中,这一开关已统一收编进更通用的scalarsMap机制(BuildContextOptions中已不存在dateScalarMode,见 src/schema/build-context.ts#L18-L42),用法等价于:
import { buildSchema, GraphQLTimestamp } from "type-graphql"; const schema = await buildSchema({ resolvers, scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }], });两种写法的核心效果一致:让Date类型统一按指定格式序列化。配置之后,字段声明无需显式标注类型:
@ObjectType() class User { @Field() registrationDate: Date; }默认 ISO 行为与scalarsMap覆盖行为,在 tests/functional/scalars.ts#L268-L298 中均有测试佐证:默认生成的字段类型名为DateTimeISO;传入scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }]后字段类型名为Timestamp。此外测试还证明scalarsMap可以完全覆盖默认 Date 映射(如 tests/functional/scalars.ts#L327-L352 用自定义标量替换 Date 映射)。
2.3 ts-node 使用提醒
如果你用ts-node运行 TypeGraphQL 代码,必须加上--type-check标志执行——这是为了规避 ts-node 历史上存在的 Date 反射(design:type 元数据)问题,否则Date字段的类型可能无法被正确反射出来。
三、自定义标量:接入 ObjectId 等第三方类型
3.1 第一步:创建GraphQLScalarType实例
自定义标量的第一步是创建一个GraphQLScalarType实例,可以自己写,也可以从第三方 npm 库导入。以 MongoDB 的ObjectId为例:
import { GraphQLScalarType, Kind } from "graphql"; import { ObjectId } from "mongodb"; export const ObjectIdScalar = new GraphQLScalarType({ name: "ObjectId", description: "Mongo object id scalar type", parseValue(value: string) { return new ObjectId(value); // value from the client input variables }, serialize(value: ObjectId) { return value.toHexString(); // value sent to the client }, parseLiteral(ast) { if (ast.kind === Kind.STRING) { return new ObjectId(ast.value); // value from the client query } return null; }, });三个回调各自负责一段数据旅程:
serialize:服务端数据 → 客户端(响应序列化,例如把ObjectId转成十六进制字符串)parseValue:客户端输入变量(variables)→ 服务端运行时对象parseLiteral:客户端查询中的字面量(AST 节点)→ 服务端运行时对象,通常需要按ast.kind判断字面量类型
仓库测试辅助文件 tests/helpers/customScalar.ts 也展示了同样结构的极简自定义标量,其serialize返回字符串"TypeGraphQL serialize"、parseLiteral返回"TypeGraphQL parseLiteral",用于验证整个读写链路。
3.2 第二步:在字段装饰器中显式使用
创建好标量后,在@Field中显式指定即可:
// import the earlier created const import { ObjectIdScalar } from "../my-scalars/ObjectId"; @ObjectType() class User { @Field(type => ObjectIdScalar) // and explicitly use it readonly id: ObjectId; @Field() name: string; @Field() isOld: boolean; }测试 tests/functional/scalars.ts#L216-L244 完整验证了自定义标量的行为:查询returnScalar时serialize被调用;查询参数argScalar(scalar: "test")时parseLiteral被调用,证明自定义标量在返回值和入参两个方向上都正常工作。
3.3 第三步(可选):用scalarsMap实现零注解自动映射
如果不想在每个字段上都写type => ObjectIdScalar,可以声明"反射属性类型 ↔ 标量"的关联关系,让 TypeGraphQL 自动匹配:
@ObjectType() class User { @Field() // magic goes here - no type annotation for custom scalar readonly id: ObjectId; }只需在buildSchema中注册映射表:
import { ObjectId } from "mongodb"; import { ObjectIdScalar } from "../my-scalars/ObjectId"; import { buildSchema } from "type-graphql"; const schema = await buildSchema({ resolvers, scalarsMap: [{ type: ObjectId, scalar: ObjectIdScalar }], });从源码看,scalarsMap的每个条目是{ type: Function; scalar: GraphQLScalarType }结构(src/schema/build-context.ts#L11-L14),构建 schema 时被存入BuildContext.scalarsMaps。convertTypeIfScalar在把 TS 类型转为 GraphQL 类型时会优先查找该映射表,命中即返回对应的自定义标量,其次才走String/Boolean/Number/Date的内置映射(src/helpers/types.ts#L28-L49)。也就是说,scalarsMap的优先级高于内置映射,甚至可以覆盖Date的默认映射——这正是 2.2 节中scalarsMap取代dateScalarMode能成立的根本原因。
3.4 自动映射的硬性限制
这种"零注解"方式能否生效,取决于 TypeScript 反射机制能否处理你的属性类型。属性类型必须是class(如ObjectId),不能是枚举(enum)、联合类型(union)或接口(interface)——因为design:type元数据只对类类型有可靠的反射结果。若反射拿不到类型,TypeGraphQL 会因缺少显式类型而无法推断(findType中会抛出NoExplicitTypeError,见 src/helpers/findType.ts#L64-L66),此时仍需回到 3.2 节的显式声明方式。
四、小结与选型建议
围绕标量类型,可以按以下规则决策:
| 场景 | 推荐做法 |
|---|---|
Int/Float/ID字段 | 使用Int、Float、ID别名,或依赖Number自动映射为Float |
string/boolean普通属性 | 直接留空@Field()自动反射 |
getter 方法、反射退化为Object的字段 | 显式写type => String/Boolean/Number |
Date字段(默认 ISO) | 无需声明;需要时间戳格式时用scalarsMap映射到GraphQLTimestamp |
| MongoDB ObjectId 等第三方类型 | 创建GraphQLScalarType后显式声明,或用scalarsMap全局注册实现自动映射 |
涉及的具体源码与测试位置:别名定义 src/scalars/aliases.ts、标量导出 src/scalars/index.ts、类型转换核心 src/helpers/types.ts#L28-L49、构建上下文 src/schema/build-context.ts#L18-L42、完整功能测试 tests/functional/scalars.ts。掌握这些机制后,无论是基础字段还是 ObjectId、BigInt 等特殊类型,都能在 TypeGraphQL 中自然、类型安全地融入 GraphQL schema。
- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
相关推荐
Windows 11终极优化指南:用Win11Debloat重获系统控制权
Windows 11终极优化指南:用Win11Debloat重获系统控制权 你的Windows 11电脑是否变得越来越"臃肿"?开机时间越来越长,后台进程悄悄吃
后端GraphQLAPI设计TypeGraphQL 标量(Scalar)类型完全指南:内置别名、Date 标量与自定义标量(version-0.17.1)
TypeGraphQL 标量(Scalar)类型完全指南:内置别名、Date 标量与自定义标量(version 0.17.1) 本篇指南以 TypeGraphQ
后端GraphQLAPI设计TypeGraphQL 标量类型(Scalars)完全指南:内置别名、自动推断与自定义 Scalar 实战
TypeGraphQL 标量类型(Scalars)完全指南:内置别名、自动推断与自定义 Scalar 实战 导读 本文围绕 TypeGraphQL 的标量(Sc
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考