Skip to content

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

@ericbiewener

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

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