Skip to content

Incomplete interface fragment coverage generates types vulnerable to non-breaking schema changes #10607

Description

@ericbiewener

Description

When querying a field that returns an interface type, if the query uses inline fragments that don't cover all possible implementations, GraphQL Codegen generates types that are fragile to non-breaking schema changes.

Specifically: if the schema later adds a new type implementing the interface (a non-breaking change), code that previously type-checked will suddenly fail, even though the query itself remains valid and the schema change was non-breaking.

Reproduction

Initial Schema

interface FinderCollectionSearchableModule {
  searchRefinement: SearchRefinement
  # ... other fields
}

type MotorsCompatibilityFinderCollection implements FinderCollectionSearchableModule {
  searchRefinement(input: MotorsSearchRefinementInput): SearchRefinement
  # ... other fields
}

type SearchableModule {
  finderCollection: FinderCollectionSearchableModule
}

Query with Incomplete Fragment Coverage

fragment SearchableReducedModuleFields on SearchableModule {
  finderCollection {
    ... on MotorsCompatibilityFinderCollection {
      searchRefinement {
        refinementSelectors {
          name
        }
      }
    }
  }
}

Generated Types (Before Schema Change)

With only one implementation of FinderCollectionSearchableModule, codegen generates:

type finderCollection = {
  __typename: 'MotorsCompatibilityFinderCollection';
  searchRefinement: { refinementSelectors: Array<{ name: string }> };
} | null;

Code Using These Types

// This compiles fine
const selectors = data.finderCollection?.searchRefinement?.refinementSelectors;

Non-Breaking Schema Change

A new type is added that implements the same interface:

type LiveEventsFinderCollection implements FinderCollectionSearchableModule {
  searchRefinement: SearchRefinement  # Same field, different signature
  # ... other fields
}

This is a non-breaking change - it only adds a new possible type.

Generated Types (After Schema Change)

Now codegen generates:

type finderCollection = 
  | { __typename: 'MotorsCompatibilityFinderCollection'; searchRefinement: {...} }
  | { __typename: 'LiveEventsFinderCollection' }  // No searchRefinement field!
  | null;

Result

The exact same code now fails type checking:

// TypeScript error: Property 'searchRefinement' does not exist on type 
// '{ __typename: 'LiveEventsFinderCollection' }'
const selectors = data.finderCollection?.searchRefinement?.refinementSelectors;

The Problem

The original query had incomplete fragment coverage, but GraphQL Codegen generated types as if the set of implementations was closed/fixed. When a new implementation was added (non-breaking change), the types changed in a breaking way.

Expected Behavior

GraphQL Codegen should handle incomplete interface fragment coverage in one of these ways:

  1. Warn/error when inline fragments don't cover all possible implementations of an interface
  2. Generate defensive types that account for unknown implementations, e.g.:
    type finderCollection = 
      | { __typename: 'MotorsCompatibilityFinderCollection'; searchRefinement: {...} }
      | { __typename: string }  // unknown implementation
      | null;
  3. Provide a configuration option like strictInterfaceFragments to enforce exhaustive coverage
  4. Generate a union that includes a catch-all for unqueried implementations

The key principle: types should be resilient to non-breaking schema changes. Adding a new type that implements an existing interface should not break existing type-checked code.

Environment

  • @graphql-codegen/cli: 5.0.5
  • @graphql-codegen/typescript: 4.1.5
  • @graphql-codegen/typescript-operations: 4.5.1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions