Why does a typed client generator inject __typename into a selection on a union?
answer
- A union declares no fields of its own
- Two entries, two different sets of keys
- The payload must say which member it is
- One alternative per selected member
- Literal type name narrows the branch
basics
~10 sA union's members share no fields, so each response entry carries only that member's keys. __typename is the runtime discriminant, letting the generator emit a tagged union the caller can match on exhaustively.
solid answer
~50 sA union has no fields of its own, so a document reaches its members through inline fragments and each response object carries only the keys of whichever member was resolved. A static type therefore cannot be one flat shape — it has to be a set of alternatives, and the caller needs a value in the payload to tell them apart. `__typename` is that value: a meta-field, available on any composite selection set and the only field selectable directly on a union, returning the concrete object type's name as a string. Generators inject it whether or not you wrote it, and emit one alternative per selected member with `__typename` narrowed to that member's literal name, so the host language can discriminate and warn about an unhandled case. The same applies to interfaces when members are reached through inline fragments. Without the discriminant the generator can only emit one merged shape with every member's fields optional.
code
graphql · 10 linesunion ScheduleEntry = Appointment | BlockedSlot | OnCallShift
query ClinicianTimeline($clinicianId: ID!, $day: Date!) {
scheduleEntries(clinicianId: $clinicianId, day: $day) {
__typename
... on Appointment { id startsAt patient { displayName } }
... on BlockedSlot { id reason }
... on OnCallShift { id rotaCode }
}
}go deeper
Know that __typename returns the concrete type's name and that a union is queried through inline fragments. Recalling that it is the one field selectable directly on a union already puts you ahead at this level.
Explain why the generated type must be a set of alternatives and how the discriminant narrows them, including that generators inject __typename for you. Interviewers want the link between the runtime string and compile-time exhaustiveness.
Show awareness of the second-order effects: the transmitted document differs from the source, so document hashing and allowlists must use build output, and a type rename silently breaks every generated consumer pinned to the old literal.
Own the consequence for schema evolution. Once every client's generated code branches on type names, those names are part of the public contract and belong in the breaking-change policy alongside field removals, with the same deprecation and usage-window discipline.
## The shape problem Suppose a hospital appointment graph exposes a clinician's day as a mixed timeline: ```graphql union ScheduleEntry = Appointment | BlockedSlot | OnCallShift type Query { scheduleEntries(clinicianId: ID!, day: Date!): [ScheduleEntry!]! } ``` A union declares no fields. There is no field common to `Appointment`, `BlockedSlot` and `OnCallShift` that a document could select on the union directly, so the only way to ask for anything is to say *if it is this member, give me these fields*, using inline fragments: ```graphql query ClinicianTimeline($clinicianId: ID!, $day: Date!) { scheduleEntries(clinicianId: $clinicianId, day: $day) { ... on Appointment { id startsAt patient { displayName } } ... on BlockedSlot { id reason } ... on OnCallShift { id rotaCode } } } ``` At execution the server resolves the concrete type of each entry and collects only the fragments that apply. So one element of the list is `{ id, startsAt, patient }` and the next is `{ id, reason }`. There is no single object shape covering both, and — crucially — nothing in those payloads *says* which one you are holding. `reason` being absent is not proof: a nullable field can be absent for other reasons. ## What __typename gives you `__typename` is a meta-field defined by the specification. It is implicitly available on the selection set of any object, interface or union type and returns a `String!` — the name of the **concrete object type** of the value being resolved. On a union it is the exception to the no-common-fields rule: it is the one field a document may select directly on the union, without an inline fragment. That makes it the discriminant. Add it, and each element is self-describing: ```json {"__typename": "BlockedSlot", "id": "bs-4471", "reason": "Theatre list"} ``` A generator can now emit a **tagged union**: one alternative per selected member, each with its `__typename` member narrowed to that member's literal name rather than to a general string. In a host language with sum types or literal-typed discriminants, matching on that member narrows the value to exactly one alternative's fields, and the compiler can flag a branch you forgot. This is why generators add `__typename` to abstract-typed selection sets automatically. It is not a convention invented by tooling — the meta-field is specified — but *injecting it* is a tooling behaviour, and it means the document that reaches the server is not byte-identical to the document you wrote. That matters if you also hash documents for a persisted-document workflow: the hash must be computed over the transformed text the client actually sends. ## Interfaces are the same problem, mostly An interface *does* have common fields, so a selection on one can be a single flat shape when you only select interface fields. The moment you add inline fragments to reach member-specific fields, you are back to alternatives and need the same discriminant. Generators typically inject `__typename` on any abstract-typed selection set for that reason, and often on object-typed ones too, since a normalized cache wants it for identity — a separate concern from typing. ## What you get without it Drop the discriminant and a generator has one honest option: emit a single merged shape with every member's fields marked optional. Then the caller writes `if (entry.reason != null)` and hopes, exhaustiveness checking is gone, and adding a fourth member to the union silently compiles. The value of the tagged union is precisely that the fourth member breaks the build. Two smaller consequences worth naming. Selecting `__typename` costs nothing on the server — it is answered from the executor's own knowledge of the resolved type, not from a resolver — so there is no reason to strip it for performance. And the string it returns is a **type name**, not a stable identifier: renaming a type in the schema changes the discriminant value and breaks a client whose generated alternatives were pinned to the old literals, which is one more reason type renames belong in the breaking-change column.
- Does an interface selection need the same discriminant?Only once it branches. A selection made purely of interface fields is one flat shape and needs nothing. As soon as inline fragments pull in member-specific fields, the result is again a set of alternatives and needs `__typename` to tell them apart. Generators usually inject it on every abstract-typed selection set rather than deciding case by case.
- The generator rewrites the document before it is sent. Where does that bite?Anywhere the exact document text matters. If documents are hashed for a persisted-document workflow, the hash has to be taken over the transformed text the client actually transmits, not the source you wrote — otherwise the server looks up a hash it has never registered. The same applies to any allowlist built from source documents rather than from build output.
- What breaks in generated clients when a type in the union is renamed?The discriminant value changes. Generated alternatives pin `__typename` to a literal type name, so a client built against the old name stops matching and falls through to whatever its default branch does. That makes a type rename a breaking change for every generated consumer, even though no field was removed.
A parcel with no label could hold anything; the label does not change the contents, it just lets you sort without opening every box.
saying these in an interview costs you the question
- Thinks a union has common fields to select
- Says __typename must be resolved by a server resolver
- Believes absent fields are enough to tell members apart
- Treats __typename as a stable object identifier
- Thinks the generator sends your document unchanged
- Assumes an interface selection always needs branching