标量是GraphQL类型系统的最底层——它们是不可再分的值。GraphQL内置了五个标量:
| 标量 | 说明 | 示例值 |
|---|---|---|
Int |
32位整数 | 42 |
Float |
浮点数 | 3.14 |
String |
UTF-8字符串 | "hello" |
Boolean |
布尔值 | true |
ID |
序列化字符串,唯一标识 | "user_1001" |
除了内置标量,你可以定义自定义标量。自定义标量需要在Schema中声明,并在服务端代码中实现序列化/反序列化逻辑:
scalar DateTime scalar Email scalar URL scalar JSON scalar Currency
自定义标量的实际用途是提供运行时验证。比如 Email 标量在输入时验证格式、输出时序列化为字符串;DateTime 标量在输入时解析ISO 8601字符串、输出时格式化为指定格式。这比在每个字段上手动验证要系统化得多。
// Apollo Server 自定义标量实现 const GraphQLDateTime = new GraphQLScalarType({ name: 'DateTime', serialize: (value) => value.toISOString(), parseValue: (value) => new Date(value), parseLiteral: (ast) => { if (ast.kind === Kind.STRING) return new Date(ast.value); throw new Error(`DateTime must be a string, got ${ast.kind}`); } });
对象类型是GraphQL的核心构造。它定义一组命名字段,每个字段有自己的类型:
type User { id: ID! name: String! email: String! avatarUrl: String role: UserRole! posts(limit: Int = 10, offset: Int = 0): [Post!]! followers: [User!]! settings: UserSettings }
对象类型的字段类型可以是标量、其他对象类型、列表([Type])、或者非空约束(Type!)。! 是非空标记——它表示查询执行时该字段保证有值。如果解析器返回了 null,GraphQL引擎会在该位置插入 null 并继续执行(不会抛异常,因为GraphQL的错误处理是部分执行的)。
列表类型的组合规则:
[String] — 可能为null的列表,列表元素也可能为null[String!] — 可能为null的列表,但列表中每个元素保证非null[String]! — 列表本身保证非null,但元素可能为null[String!]! — 列表和元素都保证非null枚举用于定义一组有限的、预定义的值。它让API的约束显式化——客户端不需要猜某个字符串字段接受什么值,枚举本身列出了所有合法选项。
enum UserRole { ADMIN EDITOR VIEWER GUEST } enum PostStatus { DRAFT PUBLISHED ARCHIVED } enum SortDirection { ASC DESC } type Query { users(role: UserRole, sortBy: SortDirection): [User!]! posts(status: PostStatus): [Post!]! }
枚举在Schema中定义,在查询中使用枚举值(不加引号):
{ users(role: ADMIN, sortBy: ASC) { name email } }
枚举值在序列化时是字符串,但GraphQL的类型系统确保只有预定义的值能通过验证。
接口定义了一组共享字段,多个对象类型可以实现(implements)同一个接口。它是GraphQL中实现多态和代码复用的核心机制。
interface Node { id: ID! } interface Timestamped { createdAt: DateTime! updatedAt: DateTime! } interface Commentable { comments(limit: Int = 10): [Comment!]! commentsCount: Int! } # 多个类型实现同一接口 type Post implements Node & Timestamped & Commentable { id: ID! title: String! content: String! createdAt: DateTime! updatedAt: DateTime! comments(limit: Int): [Comment!]! commentsCount: Int! } type Video implements Node & Timestamped & Commentable { id: ID! url: String! duration: Int! thumbnailUrl: String! createdAt: DateTime! updatedAt: DateTime! comments(limit: Int): [Comment!]! commentsCount: Int! } type Comment implements Node & Timestamped { id: ID! body: String! createdAt: DateTime! updatedAt: DateTime! }
接口在查询中的意义是:当一个字段返回接口类型时,客户端需要用内联片段来选择特定实现类型的字段:
{ feed { id ... on Post { title content } ... on Video { url duration thumbnailUrl } commentsCount } }
接口让Schema的设计更加结构化——你可以查询所有实现了 Commentable 接口的类型,确保它们都有 comments 和 commentsCount 字段。
联合类型允许一个字段返回几种不同类型中的任意一种。它和接口的区别是:接口要求实现类型必须包含接口定义的所有字段,而联合类型只要求返回值是其中一种类型,不要求共享字段。
union Media = Photo | Video | Audio union SearchResult = User | Post | Tag union Notification = LikeNotification | CommentNotification | FollowNotification
查询联合类型时,必须使用内联片段或带类型条件的片段:
{ search(query: "react") { __typename # "User" | "Post" | "Tag" ... on User { id name bio } ... on Post { id title author { name } } ... on Tag { id name postCount } } }
__typename 字段在联合类型查询中几乎是必需的——客户端用它来判断返回值的具体类型,然后走不同的渲染逻辑。
输入类型专门用于Mutation(和Query)的参数。它的语法与对象类型类似,但字段都是输入字段,不是输出字段。输入类型之间可以嵌套。
input CreatePostInput { title: String! content: String! tags: [String!] = [] status: PostStatus = DRAFT scheduledAt: DateTime } input UpdateProfileInput { name: String bio: String avatarUrl: String website: URL } type Mutation { createPost(input: CreatePostInput!): Post! updateProfile(input: UpdateProfileInput!): User! }
输入类型和对象类型的区别:
input 定义,不能引用接口和联合类型type 定义,可以引用接口和联合类型这个分离虽然看起来繁琐,但解决了参数传递的结构化问题——与其把十几个平铺参数传给Mutation,不如用一个输入类型把它们组织起来。
这些类型构造组合在一起,构成了一个表达力很强的类型系统。在实际Schema设计中,接口用于定义共享契约(如 Node、Timestamped),联合类型用于多态返回值(如搜索结果),枚举用于约束字段值,输入类型用于结构化Mutation参数,自定义标量用于领域特定的值验证。

理解每种类型的适用场景,是设计好Schema的关键。