4、GraphQL 类型系统:标量类型、对象类型、枚举类型、列表类型、非空类型

类型系统,说白了就是 GraphQL 的骨架。你想想看,如果没有类型约束,前端随便传个字段,后端随便返回个数据,那跟 REST 接口的「文档与实现不符」有什么区别?GraphQL 之所以能成为强类型查询语言,靠的就是这套类型系统。

我个人习惯,在设计 API 之前,先把类型定义写好。就像盖房子先画图纸一样,类型定义清楚了,后面的 resolver 写起来就顺风顺水。今天咱们就把这五种核心类型掰开揉碎讲清楚。

标量类型:最基础的数据单元

标量类型,就是 GraphQL 里的「原子」。它不能再往下拆分了。就像你写代码时的 StringInt,是最小的数据单位。

GraphQL 默认提供了五个标量类型:

类型 说明 示例
Int 32 位有符号整数 42
Float 双精度浮点数 3.14
String UTF-8 字符串 "Hello"
Boolean 布尔值 true
ID 唯一标识符,序列化时当作字符串 "abc123"

这里有个坑,我刚开始用的时候踩过。ID 类型虽然序列化后是字符串,但它和 String 有本质区别。ID 表示「唯一标识」,不应该被用于展示或计算。我曾经在项目里把 ID 当 String 用,结果前端做排序时出了问题——因为 ID 的语义是「标识」而不是「文本」。

核心要点:标量类型是叶子节点,查询到标量类型就到底了,不能再往下查子字段。

对象类型:组合数据的核心

对象类型,就是你把多个标量类型组合起来。比如一个「用户」有名字、年龄、邮箱,这些字段组合在一起,就是一个对象类型。

定义方式很简单:

type User {
  id: ID!
  name: String!
  age: Int
  email: String
}

看到那个感叹号了吗?那是非空标记,咱们后面细说。先看对象类型本身——它就像 TypeScript 里的 interface,定义了数据的形状。

我在项目中遇到过一个问题:对象类型可以嵌套。比如 User 里有个 posts 字段,类型是 [Post],Post 又引用了 User。这就形成了循环引用。GraphQL 完全支持这种模式,但要注意,你的 resolver 里要处理好数据加载,别搞出死循环。

小技巧:对象类型的字段名建议用驼峰命名,和 JavaScript 风格保持一致。我见过用下划线的项目,前端每次都要做字段映射,很麻烦。

枚举类型:限定可选值

枚举类型,说白了就是「只能从这几个值里选一个」。比如用户的角色:管理员、普通用户、访客。用枚举来定义,比用字符串更安全。

enum Role {
  ADMIN
  USER
  GUEST
}

type User {
  id: ID!
  name: String!
  role: Role!
}

为什么推荐用枚举?我举个例子。以前有个项目,用户角色字段用 String 存,结果前端传了个 "admin"(小写),后端存的是 "ADMIN"(大写),两边对不上,查了半天 bug。用枚举之后,GraphQL 会在查询阶段就校验值是否合法,不合法直接报错,根本不会走到业务逻辑里。

注意:枚举值在 GraphQL 中默认是大写形式。如果你需要小写,可以在服务端做转换,但 schema 里建议保持大写,这是社区惯例。

列表类型:处理多个数据

列表类型用方括号表示,比如 [User] 表示「一组用户」。它告诉 GraphQL:这个字段返回的不是单个值,而是一个数组。

type Query {
  users: [User]
  posts: [Post!]
  comments: [Comment!]!
}

这里有个容易混淆的地方:[User][User!][User]! 含义完全不同。

  • [User]:列表本身可以为 null,列表里的元素也可以为 null
  • [User!]:列表本身可以为 null,但列表里的元素不能为 null
  • [User]!:列表本身不能为 null,但列表里的元素可以为 null
  • [User!]!:列表本身不能为 null,列表里的元素也不能为 null

嗯,这里要注意。我曾经在项目里用了 [User],结果数据库查询返回了 [null, null],前端遍历时直接报错。后来我改成 [User!]!,确保要么返回空数组,要么返回全是有效用户的数组,问题就解决了。

非空类型:用感叹号守住底线

非空类型,就是字段名后面的那个感叹号 !。它表示这个字段永远不会返回 null。如果 resolver 返回了 null,GraphQL 会直接报错。

type Article {
  id: ID!
  title: String!
  content: String
  author: User!
}

你看,idtitle 后面有感叹号,表示它们必须有值。content 没有感叹号,表示可以为空(比如草稿文章可能没有内容)。author 有感叹号,表示每篇文章必须有一个作者。

我个人习惯,对于业务上「必须有」的字段,一律加非空标记。比如用户 ID、创建时间、主键等。这能帮你提前发现数据问题,而不是等到前端展示时才发现「咦,这里怎么是空的」。

实战建议:非空类型是 GraphQL 类型系统的「安全网」。多用非空,少用可选。但也要注意,如果字段在业务上确实可能为空(比如用户的中间名),就不要强行加非空,否则 resolver 会频繁报错。

五种类型如何配合使用?

实际项目中,这五种类型是混在一起用的。我给你看一个完整的例子:

enum OrderStatus {
  PENDING
  SHIPPED
  DELIVERED
  CANCELLED
}

type OrderItem {
  productId: ID!
  quantity: Int!
  price: Float!
}

type Order {
  id: ID!
  status: OrderStatus!
  items: [OrderItem!]!
  total: Float!
  createdAt: String!
}

type Query {
  order(id: ID!): Order
  ordersByStatus(status: OrderStatus!): [Order!]!
}

这个例子里,OrderStatus 是枚举,OrderOrderItem 是对象类型,IDIntFloatString 是标量类型,[OrderItem!]![Order!]! 是列表类型,几乎每个字段都用了非空类型。

你想想看,如果不用枚举,status 字段写成 String,那前端传个 "shipped" 还是 "SHIPPED" 就全靠默契了。如果不用非空,items 字段可能返回 null,前端还得做判空处理。类型系统把这些都管住了,开发效率自然就上去了。

我的经验:刚开始学 GraphQL 时,别急着用自定义标量类型(比如 Date、JSON)。先用好这五种基础类型,等项目复杂了再考虑扩展。我见过新手一上来就定义一堆自定义标量,结果序列化反序列化搞得很复杂,反而增加了维护成本。

好了,这五种类型就是 GraphQL 类型系统的基石。理解它们,你就能看懂 90% 的 GraphQL schema 了。下一章咱们聊聊如何用这些类型定义查询和变更操作,到时候你会看到它们是怎么真正跑起来的。