skip to content

What inputs does a GraphQL typed client generator need, and what does it emit?

level: juniorimportance: must knowfreq 58%

answer

  1. Two inputs, not one
  2. The schema is only half of it
  3. Your own documents are the other half
  4. Result shape follows the selection set
  5. Per operation: variables plus result

basics

~20 s

Two inputs: the schema, as an SDL file or as an introspection result, and the client's own operation documents. It emits, per operation, a variables type and a result type shaped by that operation's selection set.

solid answer

~50 s

A typed client generator takes two things. First the schema, supplied either as SDL text or as the result of an introspection query against a running server. Second the client's *own* operation documents — the queries, mutations, subscriptions and named fragments this application actually sends. From the pair it emits, per operation, a variables type derived from the operation's variable definitions and a result type derived from its selection set, plus shared types for the enums and input objects those operations touch. The result type mirrors the **selection set**, not the schema's object type: an operation asking a hospital appointment graph for `appointment { id clinician { fullName } }` yields a type with exactly those two branches, and a different operation over the same `Appointment` type yields a different type. All of this happens at build time; nothing in the generated code re-checks the response at runtime.

code

graphql · 10 lines
graphql
query ClinicianDayView($clinicianId: ID!, $day: Date!) {
  clinician(id: $clinicianId) {
    fullName
    appointments(day: $day) {
      id
      startsAt
      patient { displayName }
    }
  }
}

go deeper

for a junior

Be ready to name both inputs — the schema and your own operation documents — and to say that the result type follows the selection set. Interviewers ask this to check you understand why a client cannot be typed from the schema alone.

for a middle

Explain the mechanics: response keys and aliases decide member names, variable definitions decide the variables type, and enums and input objects come across by reference. Be able to say why two operations on one type produce two unrelated shapes.

for a senior

Show you treat the generated artefact as a snapshot with an expiry date. Talk about pinning the schema input, running generation in the build rather than by hand, and what actually happens on the day the schema moved and the artefact did not.

for a principal

Own the policy question: where the schema snapshot comes from, who publishes it, and whether every client team regenerates from the same pinned artefact. The tradeoff is coupling client builds to a schema release versus letting each team drift on its own copy.

## Why two inputs and not one It is tempting to think of a typed client generator as a schema-to-types translator, the way a tool that reads a database schema emits a row class per table. GraphQL does not work that way, and the reason is the whole point of the query language: **the server declares a graph, the client declares a slice of it.** The schema says an `Appointment` has fourteen fields. The document your screen actually sends asks for three of them. A type that carried all fourteen would be a lie — the other eleven are simply not keys in the response object. So the generator needs both halves. The schema tells it what is legal, what each field's type and nullability are, and what the enums and input objects look like. The documents tell it which of that legal surface this application asked for. Only the intersection can be typed honestly. ## The schema half The schema arrives in one of two forms. As **SDL** — a text file of type definitions, usually committed to the repository or published as a build artefact. Or as an **introspection result** — the JSON a server returns when queried through the introspection meta-fields, which a generator can fetch by pointing at a running endpoint. Both describe the same type system; they are not equally complete, and which one you feed the tool has consequences, but for the basic shape of the output either will do. ## The document half The second input is a set of executable documents: named operations and the named fragments they spread. Anonymous documents are awkward for a generator, because the operation name is what the generated type gets named after — this is why codegen setups almost always require every operation to carry a name. ## What comes out For each operation the generator emits a pair: * A **variables type**, built from the operation's variable definitions. A variable declared non-null with no default becomes a required member; a nullable variable becomes an optional one. * A **result type**, built by walking the selection set. Every selected field becomes a member; the member's name is the **response key** — the alias when one is written, otherwise the field name — and its type is the schema type of the field, recursively expanded for object-typed fields. Alongside those it emits shared declarations for the schema's enums and input object types, because those appear inside variables and results by reference rather than by selection. ```graphql query ClinicianDayView($clinicianId: ID!, $day: Date!) { clinician(id: $clinicianId) { fullName appointments(day: $day) { id startsAt patient { displayName } } } } ``` That single document produces one variables type with a required `clinicianId` and a required `day`, and one result type nested exactly three levels deep — clinician, appointments, patient — with `displayName` and nothing else under `patient`, even though the schema's `Patient` type has a dozen more fields. ## Two consequences people miss **The result type is per-operation, not per-type.** A second query that selects `patient { displayName dateOfBirth }` gets a *different*, unrelated generated shape. Some generators let a named fragment become a reusable named type, which is why fragment colocation is a common house style: it is the only way to get a shareable, non-duplicated piece of a result type. **Aliases move the key.** Writing `morning: appointments(day: $day)` renames the member in the response and therefore in the generated type. The generated code follows the document text, not the schema. ## What generation is not Generated code is a **compile-time assertion**, not a runtime guard. Nothing in it validates the bytes that come back. If the schema changed after generation — a field removed, a type narrowed — the compiler still sees the old promise and the code still compiles; you find out when a member you were told exists is missing at runtime. That gap is the whole reason a small platform team wires regeneration into the build rather than running it by hand: the artefact is only as true as the schema snapshot it was produced from. It is also not a validator of your documents. Most generators will refuse to emit code for a document the schema rejects, so in practice they catch invalid selections early — but that is a side effect of needing to resolve every field's type, not a substitute for validating the shipped documents against the schema the server is actually running.

  • Two screens select different fields of the same object type. Why does the generator emit two unrelated result types rather than one?
    Because the result type describes a response, and the two responses genuinely have different keys. A shape carrying the union of both would let code read a field the second query never asked for. The usual way to share is a named fragment: many generators emit one named type per fragment definition and compose the operation types out of those, so the shared slice is written once and both operations spread it.
  • What happens to the generated member name when a field is aliased in the document?
    The member takes the alias. The response key is the alias when one is present and the field name otherwise, and the generated type follows the response key because that is what will actually be in the payload. So `morning: appointments(day: $day)` produces a member called `morning`; the schema field name appears nowhere in the generated result type.
  • If the schema changes after generation, when does the mismatch surface?
    Not until you regenerate, or until runtime. The generated code is a build-time snapshot with no runtime checks, so a removed field still type-checks against yesterday's artefact and simply is not in the payload. That is why regeneration belongs in the build and why the schema input has to be pinned to a known version rather than fetched from whatever environment happens to answer.

The schema is the whole menu; your documents are the orders your kitchen actually places. A generator types the orders, not the menu.

saying these in an interview costs you the question

  • Says the generator only needs the schema
  • Thinks the result type mirrors the schema's object type
  • Expects generated code to validate responses at runtime
  • Assumes an alias leaves the generated member name unchanged
  • Believes generated types stay true after a schema change

context