Real domains are polymorphic: a feed mixes posts and photos, a search returns users and articles, a status field allows only a handful of values. GraphQL models all of this precisely with three type-system features: interfaces, unions, and enums. Used well, they make illegal states unrepresentable in your API.
An enum restricts a field to a fixed list of symbolic values:
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Post {
status: PostStatus!
}Compared with a String, an enum is validated on the way in (a mutation passing "pubished" fails before your resolver runs), documented automatically, and generates real union/enum types in TypeScript codegen. Convention is SCREAMING_SNAKE_CASE. Adding a value is backward compatible for the server but remember old clients may not handle it, so exhaustive switch statements on the client should always include a default branch.
An interface defines fields that several object types guarantee to implement:
interface Node {
id: ID!
}
interface Content {
id: ID!
createdAt: String!
author: User!
}
type Post implements Node & Content {
id: ID!
createdAt: String!
author: User!
title: String!
}
type Photo implements Node & Content {
id: ID!
createdAt: String!
author: User!
url: String!
}
type Query {
feed: [Content!]!
}Querying an interface, you can select shared fields directly and use inline fragments for type-specific ones:
query Feed {
feed {
__typename
id
createdAt
... on Post { title }
... on Photo { url }
}
}__typename is a meta-field available on any selection; it returns the concrete type name and is how clients branch on the result.
A union says a field returns one of several types that need not share any fields:
union SearchResult = User | Post | Photo
type Query {
search(term: String!): [SearchResult!]!
}Because there are no guaranteed common fields, every data field must sit inside an inline fragment (only __typename is directly selectable). Choose an interface when types genuinely share a contract; choose a union when they are simply alternatives. A particularly powerful pattern is using unions for mutation results: union CreatePostResult = Post | ValidationError | RateLimited forces clients to handle each outcome explicitly.
The server must be able to tell which concrete type a runtime object is. In Apollo Server you supply __resolveType:
const resolvers = {
Content: {
__resolveType(obj) {
if (obj.title !== undefined) return "Post";
if (obj.url !== undefined) return "Photo";
return null; // triggers a GraphQL error
},
},
};Many teams instead store an explicit kind discriminator on records to keep this logic trivial.
__typename plus __resolveType are the client and server halves of runtime type dispatch.Next lesson: Consuming GraphQL from a Frontend, from raw fetch to Apollo Client.