GraphQL Unions â Polymorphic Return Types Without Shared Fields
In this tutorial, you will learn about Graphql Unions. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL union types represent a field that can return one of several object types, enabling polymorphic responses when the types share no common fields.
What You'll Learn
You will learn how to define unions in SDL, implement __resolveType for type resolution, query unions with inline fragments, and design polymorphic schemas effectively.
Why Unions Matter
APIs often need to return different kinds of objects from the same field. A search endpoint might return Device, Threat, User, and Scan results â completely different shapes with no shared fields. Interfaces require shared fields. Unions do not â they Express "this field is one of these types, figure out which one." DodaTech's Durga Antivirus Pro uses a SearchResult union for its global search feature, returning devices, threats, users, and help articles from a single query.
flowchart TB
A["union SearchResult =\n Device | Threat | User | Article"] --> B["Query: search(q: 'emotet')"]
B --> C["Return mixed array"]
C --> D["Device { id, name, os }"]
C --> E["Threat { id, name, severity }"]
C --> F["User { id, email, role }"]
C --> G["Article { id, title, url }"]
style A fill:#dbeafe,stroke:#2563eb
style C fill:#fef3c7,stroke:#d97706
Prerequisites: GraphQL types and schema design. Familiarity with interfaces is helpful for comparison.
Defining a Union
union SearchResult = Device | Threat | User | Article
type Query {
search(query: String!): [SearchResult!]!
}
Union members are separated by |. All members must be object types (not scalars, enums, interfaces, or other unions).
Querying Unions with Inline Fragments
query SearchDashboard($q: String!) {
search(query: $q) {
__typename # Always check which type was returned
... on Device {
id
name
os
user { displayName }
}
... on Threat {
id
name
severity
detectedAt
}
... on User {
id
email
role
devices { id name }
}
... on Article {
id
title
url
}
}
}
// Expected response
{
"data": {
"search": [
{
"__typename": "Device",
"id": "dev-001",
"name": "Office-PC",
"os": "Windows 11",
"user": { "displayName": "Alice Smith" }
},
{
"__typename": "Threat",
"id": "thr-042",
"name": "Emotet",
"severity": "CRITICAL",
"detectedAt": "2026-06-28T10:00:00Z"
}
]
}
}
The __typename field is automatically available on every GraphQL type and tells the client which union member was returned.
Union Type Resolver
const resolvers = {
SearchResult: {
__resolveType(obj) {
// Use discriminator fields to determine type
if (obj.threatType) return 'Threat';
if (obj.os) return 'Device';
if (obj.email) return 'User';
if (obj.url) return 'Article';
return null; // Unknown type
},
},
Query: {
search: (_, { query }) => {
const results = [];
const lowerQuery = query.toLowerCase();
// Search across multiple data sources
const matchedDevices = devices.filter(d =>
d.name.toLowerCase().includes(lowerQuery)
);
const matchedThreats = threats.filter(t =>
t.name.toLowerCase().includes(lowerQuery)
);
const matchedUsers = users.filter(u =>
u.email.toLowerCase().includes(lowerQuery)
);
return [...matchedDevices, ...matchedThreats, ...matchedUsers];
},
},
};
Union vs Interface Decision
# Use union when types share NO common fields
union SearchResult = Device | Threat | User
# Query: search("office") â returns any of these, none guaranteed
# Use interface when types share common fields
interface Event { id: ID! timestamp: DateTime! }
type Threat implements Event { ... }
type Scan implements Event { ... }
# Query: recentEvents â guaranteed id and timestamp on every result
Use unions when the returned types are fundamentally different (search results, feed items, notifications). Use interfaces when types share a meaningful common structure.
Unions with Multiple Interfaces
interface Node {
id: ID!
createdAt: DateTime!
}
interface Taggable {
tags: [String!]!
}
type Device implements Node & Taggable {
id: ID!
createdAt: DateTime!
tags: [String!]!
name: String!
os: String!
}
type Threat implements Node & Taggable {
id: ID!
createdAt: DateTime!
tags: [String!]!
name: String!
severity: Severity!
}
union Searchable = Device | Threat
type Query {
searchableItems: [Searchable!]!
}
query {
searchableItems {
__typename
... on Node { id createdAt }
... on Taggable { tags }
... on Device { name os }
... on Threat { name severity }
}
}
This pattern lets clients access interface fields from union members without knowing the concrete type.
Common Mistakes
1. Forgetting __resolveType
Every union must have a __resolveType resolver. Without it, GraphQL cannot determine which type to return, and all queries fail.
2. Not Including __typename in Client Queries
Without __typename, the client receives data but cannot determine which type it belongs to. Always include __typename in union queries.
3. Using Unions When Interfaces Would Work
If all union members share at least one common field, use an interface instead. Unions should be the last resort for true polymorphism.
4. Too Many Union Members
A union with 20+ members becomes unwieldy. Consider restructuring the schema or using a more generic approach.
5. Nesting Unions Inside Unions
GraphQL does not support unions of unions. Create a flat list of object types, or use interfaces within your union members.
Practice Questions
- What types can be members of a GraphQL union?
- What is the purpose of
__typenamein union queries? - How does
__resolveTypework for unions? - When should you use a union instead of an interface?
- Can a union member implement an interface?
Answers:
- Only object types (type). Scalars, enums, interfaces, and other unions are not allowed as union members.
__typenametells the client which concrete type was returned. It's required for the client to know how to handle the union member.__resolveTypereceives the resolved object and must return the name of the GraphQL type (e.g., 'Device', 'Threat') based on discriminator fields.- Use unions when the returned types share no meaningful common fields. Use interfaces when they do.
- Yes â union members can implement interfaces. Clients can query interface fields using
... on InterfaceNamewithout knowing the concrete type.
Challenge: Design a unified feed for DodaTech's dashboard that shows a mix of ThreatAlert, ScanResult, SystemUpdate, and UserAction types using a FeedItem union. Implement search, filtering by type, and pagination. Write client-side code that renders each type differently using __typename.
FAQ
Mini Project
Create a GraphQL schema for DodaTech's notification center with a Notification union of ThreatAlert, ScanComplete, SystemAnnouncement, LicenseExpiry, and UserMention. Implement __resolveType for each, a notifications query with filtering, and a client component that renders each type differently based on __typename.
What's Next
| Topic | Description |
|---|---|
| Input Types | Structured arguments for mutations |
| Arguments Guide | Query and field arguments |
| Interfaces Guide | Shared field contracts |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro