dotansimha / dotansimha/graphql-code-generator
Incomplete interface fragment coverage generates types vulnerable to non-breaking schema changes
- Dominant language
- TypeScript
- Stars
- 11.3k
- Forks
- 1.4k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 23
Description
## 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
```graphql
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
```graphql
fragment SearchableReducedModuleFields on SearchableModule {
finderCollection {
... on MotorsCompatibilityFinderCollection {
searchRefinement {
refinementSelectors {
name
}
}
}
}
}
```
### Generated Types (Before Schema Change)
With only one implementation of `FinderCollectionSearchableModule`, codegen generates:
```typescript
type finderCollection = {
__typename: 'MotorsCompatibilityFinderCollection';
searchRefinement: { refinementSelectors: Array<{ name: string }> };
} | null;
```
### Code Using These Types
```typescript
// This compiles fine
const selectors = data.finderCollection?.searchRefinement?.refinementSelectors;
```
### Non-Breaking Schema Change
A new type is added that implements the same interface:
```graphql
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:
```typescript
type finderCollection =
| { __typename: 'MotorsCompatibilityFinderCollection'; searchRefinement: {...} }
| { __typename: 'LiveEventsFinderCollection' } // No searchRefinement field!
| null;
```
### Result
The exact same code now fails type checking:
```typescript
// 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.:
```typescript
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
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.