Interfaces
Interfaces declare a set of common fields that multiple object types must implement. Use them when types share a contract (every Node has an id) or when queries should return a polymorphic result — clients fragment on the concrete type.
Interface, implements, resolveType, Node
EXAMPLE
// 1) Declare an interface
interface Node {
id: ID!
}
// 2) Object types implement it
type User implements Node {
id: ID!
email: String!
name: String
}
type Post implements Node {
id: ID!
title: String!
author: User!
}
type Comment implements Node {
id: ID!
body: String!
author: User!
}
// 3) Use as a return type
type Query {
node(id: ID!): Node
me: User
feed: [Post!]!
}
// Now node(id: ...) can return any type that implements Node — perfect for global object lookup.
// 4) Querying interfaces — fragments
`
query FetchNode {
node(id: "User:42") {
id
... on User { email name }
... on Post { title author { name } }
... on Comment { body }
}
}
`
// 5) Resolver — __resolveType
const resolvers = {
Node: {
__resolveType(obj) {
if (obj.email) return 'User';
if (obj.title) return 'Post';
if (obj.body) return 'Comment';
return null;
},
},
Query: {
node: (_p, { id }, ctx) => {
const [type, raw] = id.split(':');
if (type === 'User') return ctx.db.user.find(raw);
if (type === 'Post') return ctx.db.post.find(raw);
if (type === 'Comment') return ctx.db.comment.find(raw);
return null;
},
},
};
// __resolveType picks the concrete type. The server runs that type's field resolvers next.
// 6) Interface fields — implementers MUST include them (with at least the declared type)
// You can ALSO add fields beyond the interface — that's the whole point.
// 7) Multiple interfaces per type
interface Timestamped { createdAt: DateTime! updatedAt: DateTime! }
type User implements Node & Timestamped {
id: ID!
email: String!
createdAt: DateTime!
updatedAt: DateTime!
}
// 8) Interfaces can implement other interfaces (GraphQL 2021 spec)
interface Node { id: ID! }
interface Resource implements Node {
id: ID!
owner: User!
}
type Document implements Resource & Node {
id: ID!
owner: User!
title: String!
}
// 9) Relay-style global object identification — the canonical use case
interface Node { id: ID! }
type Query {
node(id: ID!): Node
viewer: User
}
// Clients ALWAYS request id on any Node. Relay caches by id, so a User from query A is the
// same User if returned from query B — automatic normalisation.
// 10) Interface vs Union — when to use which
// • Interface — shared FIELDS. All types have id, createdAt, owner, etc.
// • Union — no shared fields. SearchResult = User | Post | Comment
//
// You can ALSO query an interface like a union (with fragments), but interfaces give clients
// guaranteed common fields without fragments.
// 11) Polymorphic field — list returns interface
type Query {
feed: [FeedItem!]!
}
interface FeedItem {
id: ID!
createdAt: DateTime!
actor: User!
}
type LikedPost implements FeedItem {
id: ID!
createdAt: DateTime!
actor: User!
post: Post!
}
type FollowedUser implements FeedItem {
id: ID!
createdAt: DateTime!
actor: User!
followed: User!
}
// Client can ask for actor + createdAt without fragments, fragment on the concrete type for specifics.
// 12) Codegen + clients
// • @graphql-codegen produces TypeScript discriminated unions from interfaces
// • Apollo Client + Relay generate typed fragments for the polymorphism
// • Use __typename in queries so clients can tell concrete types at runtime
`
query {
feed {
__typename
id
createdAt
... on LikedPost { post { id title } }
... on FollowedUser { followed { id name } }
}
}
`
// 13) Common bugs
// • Forgot __resolveType → 'Cannot determine type of ...'
// • Implementer drops a required field → schema error at startup
// • Returning DTO without enough fields for __resolveType to discriminate — check tag or class
// • Interface field that's expensive to compute — cache or move to concrete type
// • Wide interfaces (many fields) — narrow them; one Node{id} is fine, ManyThings{...} bloats schema
// • Missing __typename on the client — can't tell variants apart at runtime
// • Adding a new implementer without updating client fragments — runtime fallback shows blank UI
// • Using interfaces when a union is cleaner (no shared fields) — readability loss
Why it matters
Interfaces let multiple types share a contract — perfect for global object identification (Node), feed-style polymorphism, and any “same shape, different details” field. Always implement __resolveType on the server so resolvers know the concrete type, and ask clients to request __typename for safe runtime discrimination.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
interface Node { id: ID! }
type User implements Node { id: ID! name: String! }
Try it Yourself »
Discussion
Loading…