大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
类型系统
这一课你将能做到
- 熟练使用 5 种内置标量与
!非空标记、列表标记组合出准确的字段类型 - 用对象类型、枚举、接口、联合、输入类型为「课时/用户/提交记录」建模
- 读懂任意一段 SDL(Schema Definition Language)并指出其中的类型错误
为什么要学
Schema 是 GraphQL 的一切:查询校验靠它、代码生成靠它、文档靠它、前后端联调也靠它。而 Schema 的砖块就是类型系统。类型写得太松(到处是 String),校验形同虚设;写得太死(滥用非空),一个字段出错整页白屏。初学者设计 Schema 的水平差距,80% 体现在类型选择上。
人话:类型系统就是合同的措辞。措辞越精确,日后扯皮越少。
先建立图像
类型家族一览:
标量 Scalar Int Float String Boolean ID (+ 自定义标量如 DateTime)
对象 Object type Lesson { title: String! }
枚举 Enum enum ProgressStatus { NOT_STARTED IN_PROGRESS DONE }
接口 Interface interface Node { id: ID! }
联合 Union union SearchResult = Lesson | Track | User
输入 Input input LessonFilter { keyword: String, difficulty: Difficulty }
可空性的四种组合(以 String 为例):
| 写法 | 含义 | 客户端处理 |
|---|---|---|
String | 可空的值或 null | 都要判空 |
String! | 一定有值 | 直接用 |
[String!] | 列表可为 null,元素不空 | 判列表空 |
[String!]! | 列表和元素都必有 | 直接用(空则 []) |
跟着做
步骤 1:为「课时」建模
用 SDL 描述本站的核心对象。注意每个 ! 都是一次承诺:
type Lesson {
id: ID! # ID 是字符串化的唯一标识,语义上不可再拆
slug: String!
title: String!
content: String # 草稿课时可能没有正文 → 可空
durationMinutes: Int!
difficulty: Difficulty!
author: User!
track: Track
createdAt: DateTime! # 自定义标量
}
enum Difficulty { BEGINNER INTERMEDIATE ADVANCED }
scalar DateTime
步骤 2:接口——抽取公共形状
课时和训练营都「可学习」,有进度:
interface Learnable {
id: ID!
title: String!
progressFor(userId: ID!): Progress
}
type Lesson implements Learnable {
id: ID!
title: String!
progressFor(userId: ID!): Progress
durationMinutes: Int! # 接口之外自己的字段
}
type Track implements Learnable {
id: ID!
title: String!
progressFor(userId: ID!): Progress
lessons: [Lesson!]!
}
查询时用内联片段取具体类型字段:
query {
node: learnable(id: "101") {
title
... on Lesson { durationMinutes }
... on Track { lessons { slug } }
}
}
步骤 3:联合——「几选一」的返回
搜索结果可能是课时、路线、用户三种东西:
union SearchResult = Lesson | Track | User
type Query {
search(keyword: String!): [SearchResult!]!
}
联合和接口的区别:联合的成员之间不需要任何公共字段,纯「或」关系;接口则要求成员实现同一组字段。
步骤 4:输入类型——给 Mutation 用
入参不能用 type,必须用 input:
input SubmitCodeInput {
lessonId: ID!
language: Language!
code: String!
clientSubmittedAt: DateTime
}
enum Language { JAVA PYTHON JAVASCRIPT GO }
为什么分开?输入不需要「返回时的关联查询能力」,且输入对象不支持接口/联合——把读写模型分开,是 GraphQL 有意的设计。
步骤 5:描述与弃用
三个引号写文档,@deprecated 平滑下线字段:
type Lesson {
"课时标题,列表页和详情页通用"
title: String!
"旧字段,请改用 durationMinutes"
duration: Int @deprecated(reason: "Use durationMinutes")
}
GraphiQL 的文档面板会原样展示这些描述——这就是「自文档化」。
讲清楚要点
!是承诺也是风险:非空字段解析失败会把 null 一路「冒泡」到最近的可空父字段,严重时整个data变 null。列表元素、聚合根 ID 用!,计算字段慎用。- ID 不是 String:语义上 ID 不透明(客户端不该解析它),序列化形式是字符串。凡唯一标识一律用
ID。 - 枚举用于封闭集合:难度、状态、语言这种「取值可数且稳定」的用枚举;随时可能扩展的(如标签)用对象类型。
- 接口 vs 联合:有公共字段用接口,纯多态返回用联合。本站「可学习对象」是接口,「搜索结果」是联合。
- input 与 type 分离:输入对象不能包含接口/联合/对象类型输出字段,强制读写模型解耦。
- 自定义标量要序列化协议:如
DateTime约定用 ISO-8601 字符串传输,服务端和客户端都要按约定解析。 - 默认 nullable 原则:新字段默认可空,将来改非空是破坏性变更;反过来从
!放松为可空则是安全的。
你可能会懵的地方
[String]!和[String!]!到底差在哪? 前者列表必有但里面可能混 null(脏数据进入响应);后者列表必有且元素必有。业务上几乎都该用后者。- 为什么不给所有字段都加
!? 一个非空字段解析抛错,null 会冒泡并可能清空整个响应。可空字段出错只影响自己——可空性就是容错边界。 - 接口和数据库表怎么对应? 不对应。Schema 面向「查询者的视角」,不是 ER 图的镜像。一张表可以拆成多个类型,多张表可以合并成一个类型。
- input 能嵌套 input 吗?
能,
input PageInput { first: Int, after: String }可以作为别的 input 的字段。但不能嵌套输出 type。 - 字段上的参数算什么类型?
字段参数本身就是「迷你输入」,如
progressFor(userId: ID!),参数类型只能是标量/枚举/input。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
唯一标识用 String | 丢失语义、代码生成类型弱 | 用 ID |
lessons: [Lesson] | null 元素混进列表,前端判空地狱 | lessons: [Lesson!]! |
状态字段用 String | 拼写错误到运行时才炸 | 用 enum |
| Mutation 入参直接用输出 type | 规范不允许,且读写耦合 | 定义专门的 input |
| 删字段而不是弃用 | 老客户端立刻崩 | @deprecated(reason:) 过渡 |
日期用 Int 时间戳 | 时区/精度争议、可读性差 | 自定义 DateTime 标量 + ISO-8601 |
卡点急救
- 启动报错 "The type of X.y must be Input Type":Mutation 参数里用了
type,改成input。 - 查询返回 null 冒泡清空一大片:某个
!字段的 resolver 抛异常或返回 null,把该字段改成可空或修 resolver。 - 枚举值报错 "Value ... does not exist":枚举在传输层是大写蛇形字符串(
IN_PROGRESS),检查大小写。
面试常问
- 接口和联合的区别?
接口强制成员实现公共字段,可按公共字段直接查询;联合是纯「或」,成员无需公共字段,查询必须用
... on Type内联片段取具体字段。 - 为什么 GraphQL 推荐字段默认可空? 可空性是错误隔离边界:局部解析失败只置空局部,不炸整个响应;同时给 Schema 演进留了「收紧」的安全空间。
!标记的 null 冒泡规则? 非空字段为 null 时,冒泡到最近的可空祖先字段并置其为 null,同时在errors中记录;若一路到根都是非空,则data为 null。
自测三问
[String]、[String]!、[String!]、[String!]!四种类型各自的语义?- 给「提交记录」的提交状态建模,用 String 还是 Enum?为什么?
- input 和 type 为什么要分开?input 里能放接口吗?
小结一句话
类型系统是 Schema 的砖块:用 ! 承诺必有、用枚举封闭取值、用接口抽象共性、用联合表达多态、用 input 隔离写模型。
延伸
- 官方文档:Schemas and Types
- 下一课:Query——字段、参数、变量、别名与片段的完整玩法。