GraphQL

Interfaces and Union Types in GraphQL

20 min Lesson 25 of 35

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:
  1. Create a Content interface with common fields (id, title, createdAt, author)
  2. Implement Article, Video, and Podcast types with type-specific fields
  3. Create a MediaAsset union for Image, Audio, and File types
  4. Design a Notification interface for ContentPublished, CommentReceived, and MentionReceived
  5. Implement __resolveType resolvers for all abstract types
  6. Write queries using inline fragments and __typename
  7. Create reusable fragments for common patterns
Test your schema with complex polymorphic queries across multiple levels of abstraction.