The Field That Does Not Exist on This Type
The Field That Does Not Exist on This Type
A search endpoint returns posts, users, and comments — three unrelated types sharing nothing but id. In SDL that is a union:
type SearchResult = Post | User | Comment
Selecting a type-specific field directly fails validation:
query Broken {
search(term: "graphql") {
title # ✗ Cannot query field "title" on type "SearchResult"
}
}
A union declares no fields of its own beyond __typename, so there is nothing for title to bind to — the validator checks the document against the declared shape, not against the data that happens to be stored.
An inline fragment narrows the selection to one member type without naming a reusable fragment first:
query Search($term: String!) {
search(term: $term) {
__typename
... on Post { id title author { name } }
... on User { id name avatarUrl }
... on Comment { id body }
}
}
... on Post reads as "when this element is a Post, select these fields." Validation now checks title against Post and avatarUrl against User, both of which declare them, so the document passes.
Two properties of the syntax are worth pinning down:
- The condition is a type, not a name.
... on Postasserts the runtime type of the element, and validation checks that assertion against the enclosing selection: spreading... on Postinside a selection ofCommentfails with Fragment cannot be spread here as objects of type Comment can never be of type Post whenever the two types cannot overlap. - Inline means there is nothing to declare elsewhere. A named fragment needs a name, a home, and a reuse argument. When a type condition appears in one query only, the inline form keeps the test next to the fields it guards, with no second file to open.
Conditions are evaluated per element during execution, so one document can carry a branch for every member type and each element still executes only its own — the other branches cost nothing for that element.
Type Conditions at Runtime
Type Conditions at Runtime
The server executes only branches whose condition matches the element's runtime type, then merges everything into one flat object per element:
{
"data": {
"search": [
{ "__typename": "Post", "id": "1", "title": "GraphQL in practice", "author": { "name": "Ada" } },
{ "__typename": "User", "id": "9", "name": "Grace", "avatarUrl": "/g.png" },
{ "__typename": "Comment", "id": "4", "body": "Nice write-up" }
]
}
}
What that response tells you:
- Shared fields belong outside the conditions.
idand__typenameare requested once and appear on every element, so a render loop can key records before it knows anything about type. - A missing key is the discriminator. The Post element has no
avatarUrlkey at all — notnull. Branch on__typenamefirst, then read type-specific keys inside that branch. - Unmatched branches produce nothing, not nulls. The User element simply omits
titleandbody; elements are never padded with nulls to look uniform. - Interfaces behave differently at the top level. Any field declared on the interface is selectable directly, and inline fragments are needed only for fields declared on one implementer. Unions expose nothing but
__typename. - Directives attach to inline fragments.
... on Post @include(if: $showPosts) { title }hides an entire member type conditionally — see include and skip directives.
If the same element must satisfy two conditions — a Post that also implements a Featured interface, selected with both ... on Post and ... on Featured — both branches execute and their fields merge into that one element. Conditions are tests, not branches of an either/or: every matching condition runs, and only non-matching ones are skipped.
Exhaustiveness, Codegen, and Named Fragments
Exhaustiveness, Codegen, and Named Fragments
Nothing forces a document to cover every member of a union. Omit ... on Comment and validation passes: comment elements come back with only the shared keys, and a renderer that assumes title exists crashes on that one element. Exhaustiveness is a client discipline, and generated types are how you enforce it:
type SearchResult =
| { __typename: 'Post'; id: string; title: string; author: { name: string } }
| { __typename: 'User'; id: string; name: string; avatarUrl: string }
| { __typename: 'Comment'; id: string; body: string };
function render(r: SearchResult): string {
switch (r.__typename) { // compiler flags any missing case
case 'Post': return r.title;
case 'User': return r.name;
case 'Comment': return r.body;
}
}
Codegen derives that discriminated union from the schema, so adding a member type turns every non-exhaustive switch into a build error — see TypeScript codegen.
Choosing between inline and named fragments:
| Form | Type condition | Use it when |
|---|---|---|
... on Post inline |
local to this document | one screen has a one-off case |
fragment PostCard on Post spread here |
reusable across documents | the same Post selection appears in several queries |
fragment Card on SearchResult |
union/interface-wide | only shared fields plus further spreads — type-specific fields still need conditions |
Named fragments with concrete type conditions spread perfectly well inside a polymorphic selection, so reuse and type conditioning are independent tools rather than an either/or.
Interview angle: "How does the client know which member type it received?" — __typename, read before branching; then explain why an omitted branch yields an element with only shared keys instead of an error, and why that makes exhaustive switches non-negotiable.
Practice Problems
search returns a union of Post, User, and Comment. Write a document that selects shared fields once, type-specific fields under conditions, and whatever the client needs to branch on render time. Then say what the response looks like for a Comment element.
Example:
Input: union SearchResult = Post | User | Comment type Post { id: ID! title: String! author: User! } type User { id: ID! name: String! avatarUrl: String } type Comment { id: ID! body: String! }
Output: query Search($term: String!) { search(term: $term) { __typename id ... on Post { title author { name } } ... on User { name avatarUrl } ... on Comment { body } } }
A union exposes only __typename, so every other field needs a type condition. id sits outside because it is common to all three members and belongs in one place.
Optimal Solution — O(n) time, O(n) space
Select __typename and the shared id at the union level, then one condition per member for member-specific fields; the generated discriminated union makes the render switch exhaustive.
query Search($term: String!) {
search(term: $term) {
__typename
id
... on Post {
title
author { name }
}
... on User {
name
avatarUrl
}
... on Comment {
body
}
}
}
function render(r) {
switch (r.__typename) {
case 'Post': return r.title;
case 'User': return r.name;
case 'Comment': return r.body;
}
} Edge Cases:
- Omitting ... on Comment — the query still validates; comment elements arrive with only shared keys
- Selecting title without a condition — validation error: no such field on SearchResult
- Adding a fourth union member later — non-exhaustive client switches must be updated or the build fails
Quiz
1. A query selects title directly inside a union selection. What is the error?
2. How does a client know which type an element of a union search result actually is?
3. An element matches none of the inline fragments in the document. What does the client receive for it?
4. What is the key structural difference between selecting a field on an interface versus a union?
Flashcards
Question
Inline fragment syntax
Click to reveal answer
Answer
... on Type { fields } — a selection set conditioned on a type, declared inline with no name.
Question
Union vs interface selection rules
Click to reveal answer
Answer
Union: only __typename plus type-conditioned spreads. Interface: declared fields directly, conditions for member-only fields.
Question
Discriminating results
Click to reveal answer
Answer
__typename tells the client the runtime type; type-specific keys are absent (not null) on non-matching elements.
Question
Shared-field placement
Click to reveal answer
Answer
Put common fields outside inline fragments so every element returns them once, then branch inside.
Question
Enforcing exhaustiveness
Click to reveal answer
Answer
Codegen emits a discriminated union; a switch on __typename turns missing members into compile errors.
Revision Notes
Key Takeaways
- 1. Type-specific fields require a type condition; unions expose only __typename at the top level
- 2. Execution applies matching branches and merges results into flat elements
- 3. Missing keys, not nulls, mark which type an element was
- 4. Exhaustive handling is enforced with generated discriminated unions, not by the server
Interview Tips
- • Write the polymorphic query from memory: __typename, one condition per member, shared fields outside
- • Explain the difference between interfaces and unions in what they expose without conditions
- • Describe how you guarantee every member type is handled — generated union plus exhaustive switch
Cheat Sheet
Inline Fragments Cheat Sheet
query Search($term: String!) {
search(term: $term) {
__typename
id # shared: outside the conditions
... on Post { title author { name } }
... on User { name avatarUrl }
... on Comment { body }
}
}
... on Type= selection set valid only when the element is that type- Union top level:
__typenameonly; interface top level: declared fields too - Non-matching branches are omitted — type-specific keys are absent, not null
- Unions/interfaces: condition required for member-specific fields, shared fields go outside
- Directives work on conditions:
... on Post @include(if: $x) { title } - Exhaustiveness: codegen discriminated union + switch on
__typename