Skip to content

GraphQL Unions — Polymorphic Return Types Without Shared Fields

DodaTech Updated 2026-06-28 6 min read

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
â„šī¸ Info

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

  1. What types can be members of a GraphQL union?
  2. What is the purpose of __typename in union queries?
  3. How does __resolveType work for unions?
  4. When should you use a union instead of an interface?
  5. Can a union member implement an interface?

Answers:

  1. Only object types (type). Scalars, enums, interfaces, and other unions are not allowed as union members.
  2. __typename tells the client which concrete type was returned. It's required for the client to know how to handle the union member.
  3. __resolveType receives the resolved object and must return the name of the GraphQL type (e.g., 'Device', 'Threat') based on discriminator fields.
  4. Use unions when the returned types share no meaningful common fields. Use interfaces when they do.
  5. Yes — union members can implement interfaces. Clients can query interface fields using ... on InterfaceName without 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

Can I add new members to a union without breaking changes?

Yes — adding new object types to a union is backward-compatible. Existing clients ignore types they don't have fragments for. However, they may show incomplete data.

What happens if __resolveType returns a name not in the union?

GraphQL throws a validation error. The field returns null (or propagates null if non-nullable). Ensure __resolveType always returns a valid member name.

How do I handle unknown types in __resolveType?

Return null for unknown types. Consider adding a Unknown fallback type to your union for unanticipated results.

Can I query a union without inline fragments?

No — you must use inline fragments (... on TypeName) to access fields of union members. Fields common to all members don't exist (unlike interfaces).

Are unions supported in all GraphQL clients?

Yes — all major clients (Apollo, Relay, urql) support unions. The __typename field is used for cache normalization and type-specific rendering.

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
âŦ… Interfaces Guide
➡ Input Types

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro