Why Your Client Types Should Derive from Queries and Fragments — Not the Schema

Loren Sands-Ramshaw
The GraphQL Guide
Published in
5 min readDec 12, 2024

When you’re building a GraphQL client in TypeScript, it’s tempting to generate your types directly from the server’s schema. After all, the schema is your API’s single source of truth, right?

Not exactly. While the schema defines every possible field that could be returned, it doesn’t guarantee which fields are actually returned in a given situation. Your React components, Vue views, or Svelte pages only receive the exact fields they query, not the entire schema. That’s why your client-side TypeScript types should be generated from your queries and fragments, not from the abstract schema.

Think of it this way: the schema is like a full restaurant menu, but your query is the actual order you place. The kitchen will only bring out what you’ve asked for, so your “plate” (the data in your UI) needs a type that reflects what’s actually served, not the entire menu.

Example: The Disconnect Between Schema and Queries

Schema:

type User {
id: ID!
name: String!
email: String!
profilePicture: String
}

Query Used by Your Component:

query GetUser {
user(id: "123") {
id
name
}
}

If you rely on a schema-generated User type, you might assume email and profilePicture are always there—only to discover at runtime that they’re missing because the query didn’t request them. By generating types directly from the GetUser query, your TypeScript type will only include id and name, perfectly matching what your component actually receives.

1. The Schema Is a Superset, Not the Actual Data

What’s the Problem?
The schema defines all possible fields, many of which your component never requests.

Why It Matters
Basing types on the schema can lead to runtime errors. Your code may assume fields like email are always present, even when they weren’t requested.

In Other Words
If you ask for less, don’t pretend you got more.

2. Truly Meaningful Type Safety

What’s the Problem?
Schema-based types don’t reflect what’s guaranteed to be there at runtime. You have to write extra checks and optional chains for fields that may not exist. Schema-based types

Why It Matters
Query-based types match what the server actually returns, so your component code can trust the type system. Fewer null checks, fewer surprises.

In Other Words
Your types should describe what you really get, not what you theoretically could get.

3. Effortless Maintenance as Queries Evolve

What’s the Problem?
As you build new features, you’ll often update your queries — adding fields, removing fields, or tweaking arguments. If your types come from the schema, you must manually synchronize them every time.

Why It Matters
By using tools like @graphql-codegen/cli to generate types from your operations, your types stay perfectly in sync whenever your queries change. No manual updates required.

In Other Words
Your types will never lie about what’s actually returned. Update the query, regenerate, and you’re done.

4. More Relevant Autocompletion and In-Editor Docs

What’s the Problem?
Schema-level types expose every possible field, cluttering your IDE’s suggestions. You might waste time searching through irrelevant fields that your query doesn’t even fetch.

Why It Matters
Query-derived types give you exactly the fields you requested. Your editor’s autocomplete and in-editor documentation become focused, accurate, and more helpful.

In Other Words
You’ll see only what’s on your plate, not the entire menu.

5. Stronger Teamwork and Less Confusion

What’s the Problem?
Onboarding new team members becomes harder when types don’t match the data actually being returned. They might write code assuming certain fields exist, only to hit runtime errors.

Why It Matters
When your types reflect real query results, everyone on the team can trust that what they see is what they get. This reduces back-and-forth between frontend and backend teams and speeds up development.

In Other Words
Your team can code confidently, without guesswork or double-checking the API docs for what’s actually returned.

6. Narrow types with __typename

What’s the Problem?
When you get a polymorphic type—an interface or union—returned from the server, you don’t know which specific object type it is, so you don’t know which fields it has. The schema doesn’t have __typename, but query-based types usually do. So if you have this schema:

interface Message {
id: ID!
title: string!
text: string!
}

type Post implements Messsage {
id: ID!
title: string!
text: string!
likes: number!
}

type Reply implements Message {
id: ID!
title: string!
text: string!
parent: Message!
}

type Query {
message(id: ID!): Message
}

With __typename, you can write and use type guards:

import type { Message, Post, Reply }

function isReply(message: Message): message is Reply {
return message?.__typename === 'Reply';
}

const message = await sendMessageQuery({id: "1"})
if (isReply(message)) {
console.log(message.parent)
}

We won’t get a build error on the last line, because TypeScript knows that message is a Reply, and thus has a parent field.

Why It Matters
__typename makes polymorphic types usable by narrowing down possibilities, allowing you to write safer, more predictable code that’s tailored to each specific type.

7. Support for aliases

What’s the Problem?

Schema types aren’t aware of field aliases, or selecting a field multiple times. Say I’m selecting the Video.artwork field twice, and using two aliases:

fragment VideoBillboard on Video { 
billboardArtwork: artwork(/* Billboard artwork params */)
}

fragment VideoLogo on Video {
logoArtwork: artwork(/* Logo artwork params */)
}

fragment SomeVideoTreatment on Video {
...VideoLogo
...VideoBillboard
}

The schema-based type will have Video.artwork, which won’t exist in the server’s response: instead, Video.billboardArtwork and Video.logoArtwork will.

Why It Matters
Query-based types ensure your types match the actual structure of the server’s response, including aliases. It also allows you to take full advantage of GraphQL’s flexibility without sacrificing type safety.

Recommended Tools and Best Practices

  • Code Generation
    Integrate @graphql-codegen/cli into your build process to generate TypeScript types directly from your queries and fragments. This ensures your types are always up to date.
  • Apollo and The Guild’s Recommendations
    The Apollo documentation and GraphQL Code Generator’s official docs recommend generating types from operations. This is an established best practice, not a personal preference.

Conclusion

Your GraphQL schema defines what could be returned, but your queries define what will be returned. By generating TypeScript types from your queries and fragments, you ensure that your type system is perfectly aligned with reality. This approach saves time, reduces confusion, and helps your team ship features with greater confidence.

What to Do Next

  • Integrate @graphql-codegen/cli into your workflow.
  • Generate types from your queries and fragments.
  • Enjoy cleaner code, fewer runtime surprises, and a happier development team.

Keep your types honest, and let your queries guide the way!

Learn more GraphQL best practices from The GraphQL Guide.

Thanks to Whitney Beck, Alan Norbauer, and Peter Stauskas for reviewing drafts of this post. 🙏😊

--

--

Loren Sands-Ramshaw
Loren Sands-Ramshaw

Written by Loren Sands-Ramshaw

GraphQL @NetflixEng, author of the @graphqlguide. Formerly @temporalio, startups, consulting, NSA.

No responses yet