Where can you spread a GraphQL fragment, and what does its type condition control?
answer
- Not everywhere - only where it could fit
- Compare two sets of types
- Possible types must intersect
- An interface condition widens, an object condition narrows
- No overlap means the document is rejected
basics
~20 sA fragment may be spread into any selection set whose type could actually be the fragment's type condition - the two must share at least one possible object type. The type condition also fixes which fields the fragment is allowed to select.
solid answer
~50 sThe type condition after `on` does two jobs. First, it scopes the fragment's own selection set: every field named inside must exist on that type, whatever type the spread site happens to have. Second, it decides where the spread is legal. Each composite type has a set of **possible types** - itself for an object, its implementations for an interface, its members for a union - and a spread is allowed only where the parent's possible types and the condition's possible types intersect. When the condition is the parent's own type, or an interface or union the parent belongs to, the fragment always applies. When the condition is a concrete type under an abstract parent, it applies only to the objects that turn out to be that type, contributing nothing to the others. When the two sets cannot overlap at all, the whole document is rejected rather than returning an empty result.
code
graphql · 16 linesquery ClaimTimeline($id: ID!) {
claim(id: $id) {
...ClaimHead
events {
__typename
...EventStamp
...PayoutDetail
...AssignmentDetail
}
}
}
fragment ClaimHead on Claim { claimNumber status }
fragment EventStamp on ClaimEvent { id occurredAt }
fragment PayoutDetail on PaymentIssued { amountCents currency }
fragment AssignmentDetail on AdjusterAssigned { adjuster { name licenceState } }go deeper
Know that the clause after on is not decoration: it names the type whose fields the fragment may select, and a fragment written for one type cannot simply be pasted into a selection on an unrelated type.
Be ready to state the overlap rule and work an example both ways - an interface fragment spread into a concrete selection, and a concrete fragment spread into an interface or union selection - and to say which of the two applies conditionally.
Demonstrate the operational consequences: choosing the widest true condition so a fragment is reusable, expecting polymorphic responses where narrowing spreads are used, and recognising a schema edit to an implements clause as something that breaks live documents.
Own the coupling this creates between schema shape and caller documents: how much of the graph you expose as interfaces and unions determines how reusable caller fragments can be, and how expensive an abstract-type change becomes once many documents spread against it.
## Two jobs for one clause `on Claim` reads like documentation. It is not: the type condition is the only thing deciding both what a fragment may select and where its spread is allowed to appear. ## Job one - the fields the fragment may name A fragment's selection set is checked against its type condition, never against the place it is spread. ```graphql interface ClaimEvent { id: ID! occurredAt: DateTime! } type PaymentIssued implements ClaimEvent { id: ID! occurredAt: DateTime! amountCents: Int! currency: String! } ``` A fragment `on ClaimEvent` selecting `id` and `occurredAt` is fine. Adding `amountCents` to it is not - even in an installation where every event ever fetched happens to be a payment, because `amountCents` is not a field of `ClaimEvent`. Static types govern; the runtime population is irrelevant. ## Job two - where the spread may appear Every composite type has a set of **possible types**: for an object type, itself; for an interface, every object type that implements it; for a union, its members. A spread is permitted where the possible types of the *parent* - the type of the selection set the spread sits in - and the possible types of the condition have at least one type in common. If they cannot overlap, no object could ever satisfy both, and the whole document is rejected before execution rather than quietly returning nothing. That one rule produces the everyday shapes: | type condition | parent type | effect | | --- | --- | --- | | object `A` | object `A` | always applies | | interface or union | an object that implements or belongs to it | always applies | | object `A` | an interface or union that `A` belongs to | applies only when the runtime object is an `A` | | interface `I` | interface `J` with shared implementations | applies to objects implementing both | | no shared possible type | anything | document rejected | The third row is the interesting one: such a fragment contributes fields *conditionally*. For an object that turns out to be some other member, the spread contributes nothing at all - not null-valued fields, nothing. ## A claims timeline ```graphql query ClaimTimeline($id: ID!) { claim(id: $id) { ...ClaimHead events { ...EventStamp ...PayoutDetail ...AssignmentDetail } } } fragment ClaimHead on Claim { claimNumber status } fragment EventStamp on ClaimEvent { id occurredAt } fragment PayoutDetail on PaymentIssued { amountCents currency } fragment AssignmentDetail on AdjusterAssigned { adjuster { name licenceState } } ``` `ClaimHead` is object into the same object. `EventStamp` widens: its condition is the very interface the parent selection is typed as, so it applies to every element. `PayoutDetail` and `AssignmentDetail` narrow: each contributes only to elements of its own type, so a timeline of five events comes back carrying three different shapes in one list. ## Unions have no fields of their own A union declares no fields. Directly inside a selection set typed as a union you may select only the meta-field `__typename`; everything else has to arrive through a fragment whose condition is one of the members, or an interface all the members implement. Candidates often assume a union behaves like an interface and exposes whatever its members have in common - it does not, even when the overlap is total. ## The disjoint case, concretely A fragment `on Policy` selecting `effectiveDate` and `expiresAt`, spread directly into a `claim { ... }` selection, is not "a spread that matches nothing". `Claim` and `Policy` are two distinct object types; their possible-type sets are `{Claim}` and `{Policy}`, and the intersection is empty. The document is rejected, no resolver runs, and the response carries errors with no data. The fix is placement rather than the fragment: spread it one level down, inside the `policy` field's own selection set, where the parent type genuinely is `Policy`. ## Practical consequences **Pick the widest condition that is still true.** A fragment on `ClaimEvent` can be reused at every site where events appear; the same fields written on `PaymentIssued` can only be spread where a payment is reachable. Widening costs nothing while the fields are genuinely shared, and it stops a screen accumulating four near-identical fragments. **A spread site is coupled to the type graph.** If a type stops implementing an interface, or leaves a union, every document that spread a fragment at a site that is now disjoint becomes invalid. From the caller's side this looks like an unrelated schema edit breaking a screen, which is exactly why schema owners treat removing an `implements` clause as a breaking change. **Narrowing makes the response polymorphic.** A caller consuming the timeline above must be prepared for elements that carry only the interface fields. Fields contributed by a fragment that did not apply are simply absent, not null. ## Saying it in an interview State the possible-types overlap rule first, then walk the widen and narrow pair with one example. Interviewers are usually probing for two things: that you know a fragment's fields are checked against its condition rather than against the spread site, and that you know a mismatched spread is a rejected document rather than an empty result.
- Can a fragment's type condition be a union, and what may such a fragment select?Yes, a union is a legal type condition, but a union declares no fields of its own, so directly inside it you may select only the meta-field `__typename`. Anything substantive has to come from further fragments whose conditions are member types, or an interface every member implements. This is where unions differ sharply from interfaces, which do expose their declared fields to any selection typed as the interface.
- A fragment on an interface is spread inside a selection on one object type implementing it. Does it always apply?Yes, unconditionally. The parent's possible types are just that one object, and the interface's possible types include it, so every object reaching that selection satisfies the condition. Nothing is skipped and nothing is conditional. It is the mirror image of the narrowing case, where a concrete condition under an abstract parent applies only to some of the objects.
- A type stops implementing an interface in the next schema release. What happens to documents spreading a fragment on that interface at sites typed as that object?Those spread sites become disjoint: the object is no longer among the interface's possible types, the intersection is empty, and every document that used the fragment there fails validation and stops working. Nothing about the fragment changed. That is why removing an `implements` clause is treated as a breaking schema change even when no field was deleted.
The type condition is a stencil cut for one shape: it fits wherever a matching shape can appear, and where the shapes cannot possibly line up you are not handed a blank page, you are told the stencil does not belong there.
saying these in an interview costs you the question
- Thinks a fragment can be spread anywhere in a document
- Selects a subtype's field inside an interface-conditioned fragment
- Believes the type condition is only documentation
- Expects a mismatched spread to return nulls, not an error
- Thinks a union fragment can select fields common to members
- Assumes a concrete-type fragment always contributes its fields