Understanding Abstract Types
GraphQL provides two powerful features for handling polymorphic data: interfaces and union types. These abstract types allow you to define fields that can return multiple different object types, making your schema more flexible and expressive. They're essential for modeling real-world scenarios where entities share common characteristics or can be of different types.
GraphQL Interfaces
An interface is an abstract type that defines a set of fields that implementing types must include:
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node {
# Required fields from Node interface
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
# Additional User-specific fields
name: String!
email: String!
posts: [Post!]!
}
type Post implements Node {
# Required fields from Node interface
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
# Additional Post-specific fields
title: String!
content: String!
author: User!
}
type Comment implements Node {
# Required fields from Node interface
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
# Additional Comment-specific fields
text: String!
author: User!
post: Post!
}
type Query {
# Return any type that implements Node
node(id: ID!): Node
# Search across all Node types
search(query: String!): [Node!]!
}
Note: Any type implementing an interface must include all fields defined in the interface with the same types. Implementing types can add additional fields beyond what the interface requires.
Querying Interface Types
Use inline fragments to query type-specific fields:
query SearchContent {
search(query: "GraphQL") {
# Common fields available on all Node types
id
createdAt
updatedAt
# Use __typename to identify the specific type
__typename
# Inline fragments for type-specific fields
... on User {
name
email
}
... on Post {
title
content
author {
name
}
}
... on Comment {
text
author {
name
}
post {
title
}
}
}
}
# Response:
{
"data": {
"search": [
{
"id": "1",
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-01-15T10:00:00Z",
"__typename": "Post",
"title": "Introduction to GraphQL",
"content": "GraphQL is a query language...",
"author": { "name": "John Doe" }
},
{
"id": "2",
"createdAt": "2026-01-16T11:30:00Z",
"updatedAt": "2026-01-16T11:30:00Z",
"__typename": "User",
"name": "Jane Smith",
"email": "jane@example.com"
}
]
}
}
Multiple Interface Implementation
A type can implement multiple interfaces:
interface Node {
id: ID!
}
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
interface Authored {
author: User!
publishedAt: DateTime
}
# Post implements all three interfaces
type Post implements Node & Timestamped & Authored {
# From Node
id: ID!
# From Timestamped
createdAt: DateTime!
updatedAt: DateTime!
# From Authored
author: User!
publishedAt: DateTime
# Post-specific fields
title: String!
content: String!
tags: [String!]!
}
# Comment implements Node and Timestamped
type Comment implements Node & Timestamped {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
text: String!
author: User!
}
type Query {
# Return any Authored content
authoredContent(authorId: ID!): [Authored!]!
# Return any Timestamped entity
recentActivity: [Timestamped!]!
}
Tip: Use multiple interfaces to compose shared behaviors. This follows the Interface Segregation Principle and makes your schema more maintainable.
Union Types
A union type represents an object that could be one of several types, without requiring shared fields:
type User {
id: ID!
name: String!
email: String!
}
type Post {
id: ID!
title: String!
content: String!
}
type Comment {
id: ID!
text: String!
}
# SearchResult can be User, Post, or Comment
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
# Resolvers
const resolvers = {
Query: {
search: async (parent, { query }, context) => {
// Search across multiple collections
const users = await context.db.users.find({ name: query });
const posts = await context.db.posts.find({ title: query });
const comments = await context.db.comments.find({ text: query });
// Return mixed array of different types
return [...users, ...posts, ...comments];
}
},
SearchResult: {
__resolveType(obj) {
// Determine which type based on object properties
if (obj.email) return 'User';
if (obj.title) return 'Post';
if (obj.text) return 'Comment';
return null;
}
}
};
Querying Union Types
Use inline fragments to access fields on specific union member types:
query SearchAll {
search(query: "GraphQL") {
__typename
... on User {
id
name
email
}
... on Post {
id
title
content
}
... on Comment {
id
text
}
}
}
# Response with mixed types:
{
"data": {
"search": [
{
"__typename": "User",
"id": "1",
"name": "GraphQL Expert",
"email": "expert@example.com"
},
{
"__typename": "Post",
"id": "10",
"title": "GraphQL Best Practices",
"content": "Here are some tips..."
},
{
"__typename": "Comment",
"id": "50",
"text": "Great explanation of GraphQL!"
}
]
}
}
Warning: Union types don't share any common fields. You must use inline fragments to access any fields. If types should share fields, consider using interfaces instead.
Interfaces vs Union Types
Choose the right abstract type for your use case:
# ✅ Use Interface when types share common fields
interface Media {
id: ID!
title: String!
url: String!
uploadedAt: DateTime!
}
type Image implements Media {
id: ID!
title: String!
url: String!
uploadedAt: DateTime!
width: Int!
height: Int!
format: String!
}
type Video implements Media {
id: ID!
title: String!
url: String!
uploadedAt: DateTime!
duration: Int!
resolution: String!
}
type Query {
media(id: ID!): Media
allMedia: [Media!]!
}
# Query can access common fields without fragments
query GetAllMedia {
allMedia {
id
title
url
uploadedAt
# Type-specific fields need fragments
... on Image {
width
height
}
... on Video {
duration
}
}
}
# ✅ Use Union when types are related conceptually but don't share fields
type TextPost {
id: ID!
text: String!
}
type ImagePost {
id: ID!
imageUrl: String!
caption: String!
}
type VideoPost {
id: ID!
videoUrl: String!
thumbnail: String!
}
union FeedItem = TextPost | ImagePost | VideoPost
type Query {
feed: [FeedItem!]!
}
# Query requires fragments for all fields
query GetFeed {
feed {
__typename
... on TextPost {
id
text
}
... on ImagePost {
id
imageUrl
caption
}
... on VideoPost {
id
videoUrl
thumbnail
}
}
}
The __typename Field
The __typename meta-field returns the name of the object type:
query GetContent {
search(query: "tutorial") {
__typename # Always available on any object type
... on Post {
id
title
}
... on Video {
id
duration
}
}
}
# Client-side handling based on __typename
function renderSearchResult(result) {
switch (result.__typename) {
case 'Post':
return <PostCard post={result} />;
case 'Video':
return <VideoPlayer video={result} />;
case 'User':
return <UserProfile user={result} />;
default:
return null;
}
}
// Apollo Client automatically includes __typename
// It's used for cache normalization and type identification
const { data } = useQuery(SEARCH_QUERY);
data.search.forEach(result => {
console.log(result.__typename); // 'Post', 'Video', or 'User'
});
Type Resolution
Implement __resolveType to determine concrete types:
// For Interfaces
const resolvers = {
Node: {
__resolveType(obj, context, info) {
// Check discriminator field
if (obj.type === 'USER') return 'User';
if (obj.type === 'POST') return 'Post';
if (obj.type === 'COMMENT') return 'Comment';
// Or check for unique fields
if (obj.email) return 'User';
if (obj.title && obj.content) return 'Post';
if (obj.text && obj.postId) return 'Comment';
// Or use instanceof (if using classes)
if (obj instanceof User) return 'User';
if (obj instanceof Post) return 'Post';
return null; // Unknown type
}
}
};
// For Union Types
const resolvers = {
SearchResult: {
__resolveType(obj) {
// Use explicit type field if available
if (obj.__typename) return obj.__typename;
// Otherwise infer from structure
if (obj.email && obj.posts) return 'User';
if (obj.title && obj.author) return 'Post';
if (obj.text && obj.post) return 'Comment';
throw new Error('Could not resolve type');
}
}
};
// Alternative: Return objects with __typename
const resolvers = {
Query: {
search: async (parent, { query }, context) => {
const users = await context.db.users.find({ name: query });
const posts = await context.db.posts.find({ title: query });
return [
...users.map(u => ({ ...u, __typename: 'User' })),
...posts.map(p => ({ ...p, __typename: 'Post' }))
];
}
}
};
Best Practice: Include a __typename or type discriminator field in your database models. This makes type resolution simple and explicit.
Polymorphic Queries
Real-world example: Activity feed with multiple content types:
interface Activity {
id: ID!
timestamp: DateTime!
actor: User!
}
type PostCreated implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
post: Post!
}
type CommentAdded implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
comment: Comment!
post: Post!
}
type UserFollowed implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
followedUser: User!
}
type LikeReceived implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
likedContent: LikeableContent!
}
union LikeableContent = Post | Comment
type Query {
# Activity feed showing all types
activityFeed(limit: Int = 20): [Activity!]!
}
# Query with fragments
query GetActivityFeed {
activityFeed(limit: 10) {
# Common fields
id
timestamp
actor {
name
avatar
}
# Type-specific fields
... on PostCreated {
post {
title
excerpt
}
}
... on CommentAdded {
comment {
text
}
post {
title
}
}
... on UserFollowed {
followedUser {
name
avatar
}
}
... on LikeReceived {
likedContent {
__typename
... on Post {
title
}
... on Comment {
text
}
}
}
}
}
# Resolvers
const resolvers = {
Query: {
activityFeed: async (parent, { limit }, { userId, db }) => {
// Fetch and merge activities from different tables
const activities = await db.activities
.find({ userId })
.sort({ timestamp: -1 })
.limit(limit);
return activities; // Already includes __typename from DB
}
},
Activity: {
__resolveType(obj) {
return obj.__typename; // 'PostCreated', 'CommentAdded', etc.
}
},
LikeableContent: {
__resolveType(obj) {
return obj.__typename; // 'Post' or 'Comment'
}
}
};
Fragment Composition
Reuse fragments for cleaner queries:
# Define reusable fragments
fragment UserInfo on User {
id
name
avatar
email
}
fragment PostPreview on Post {
id
title
excerpt
publishedAt
}
fragment CommentPreview on Comment {
id
text
createdAt
}
# Use in polymorphic query
query GetSearchResults {
search(query: "GraphQL") {
__typename
... on User {
...UserInfo
posts {
...PostPreview
}
}
... on Post {
...PostPreview
author {
...UserInfo
}
comments {
...CommentPreview
}
}
... on Comment {
...CommentPreview
author {
...UserInfo
}
}
}
}
Tip: Use fragment composition to keep polymorphic queries readable and maintainable. Define fragments once and reuse them across multiple queries.
Interface Extensions
Extend interfaces to add new implementing types:
# Base schema
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
# Extension in another module
extend interface Node {
createdAt: DateTime! # Add field to interface
}
# All implementing types must now include createdAt
type User implements Node {
id: ID!
name: String!
createdAt: DateTime! # Required after interface extension
}
# Add new type implementing existing interface
type Organization implements Node {
id: ID!
createdAt: DateTime!
name: String!
members: [User!]!
}
Exercise: Design a content management system schema using interfaces and unions:
- Create a
Content interface with common fields (id, title, createdAt, author)
- Implement Article, Video, and Podcast types with type-specific fields
- Create a
MediaAsset union for Image, Audio, and File types
- Design a
Notification interface for ContentPublished, CommentReceived, and MentionReceived
- Implement
__resolveType resolvers for all abstract types
- Write queries using inline fragments and
__typename
- Create reusable fragments for common patterns
Test your schema with complex polymorphic queries across multiple levels of abstraction.