guisehn.com

Precisely Typed React Components with GraphQL Fragments

If you’re using Apollo Client and GraphQL Code Generator, you can go beyond just typing your queries - you can make each component precisely typed to the fields it actually consumes.

This post shows how to colocate GraphQL fragments with components in React, extract their types, and guarantee end-to-end type safety: from the GraphQL schema, through the page query, down to the component props.

The problem

It’s common to type components using the entire GraphQL type, like this:

import { Product } from '@lib/graphql/codegen/graphql';

type ProductCardProps = {
  product: Product; // Full GraphQL type
};

This works, but it creates two issues:

  1. The page that renders the component often queries selecting only a subset of fields of Product. Once you pass the product to your component, there will be a type mismatch because the component expects all fields, so you’re forced to cast with as Product. This loses type safety: if you forgot to select a field that the component needs, TypeScript won’t catch it.

  2. When testing or mocking, you need to provide all fields of Product - even the ones the component doesn’t use.

What we really want is for each component to declare its own data contract.

Colocated fragments as contracts

With GraphQL Codegen, fragments are strongly typed. We can colocate a fragment next to a component, describing exactly the fields it requires:

import { gql } from "@lib/graphql/codegen";
import { GraphqlExtract } from "@lib/typeUtils";

/**
 * To render a product card, these fields must be selected
 */
export const ProductCardFragment = gql(`
  fragment ProductCardFields on Product {
    id
    name
    url
    availability
    price
  }
`);

export type ProductCardProduct = GraphqlExtract<typeof ProductCardFragment>;

Notice the helper type:

import { TypedDocumentNode } from "@apollo/client";

/**
 * Given a TypedDocumentNode, extract its result type.
 */
export type GraphqlExtract<D> = D extends TypedDocumentNode<infer T, any>
  ? T
  : never;

This little utility pulls out the data type from a TypedDocumentNode, so our fragment’s TypeScript type matches exactly what the GraphQL fragment defines.

The ProductCardProduct type in that case is automatically generated as:

{
  id: string;
  name: string;
  url: string;
  availability: string;
  price: number;
}

A strongly typed component

Now we can type our component using only the fields from its fragment:

type ProductCardProps = {
  product: ProductCardProduct;
};

export function ProductCard({ product }: ProductCardProps) {
  return (
    <div>
      <h3>
        <a href={product.url}>{product.name}</a>
      </h3>
      <p>${product.price.toFixed(2)}</p>
      {product.availability === "IN_STOCK" && <span>✔ In Stock</span>}
    </div>
  );
}

If we try to use product.description, TypeScript immediately errors, because that field isn’t part of the fragment.

This keeps the component honest: its props are exactly the data it declared in its fragment.

The page query

On the page level, we can now reference the fragment in the query:

import { gql } from "@lib/graphql/codegen";
import { useQuery } from "@apollo/client";

import { ProductCard } from "@components/product-card";

const ProductsPageQuery = gql(`
  query ProductsPage {
    products {
      ...ProductCardFields
    }
  }
`);

Note: In some setups, you might need to manually interpolate fragments with ${ProductCardFragment}. But when using a gql wrapper generated by GraphQL Codegen (like @lib/graphql/codegen), the tooling automatically injects fragment definitions. In that case, just writing ...ProductCardFields is enough.

Rendering the product list

Finally, we can wire it up in the page component:

export function ProductListPage() {
  const { data, loading, error } = useQuery(ProductsPageQuery);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong!</p>;

  return (
    <div>
      {data?.products.map((p) => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

Here’s the magic: the ProductCard expects a ProductCardProduct. The query guarantees that each p is exactly that type, because it spreads the same fragment. The compiler enforces this contract end-to-end - from the GraphQL schema, through the query, down to the component’s props.

One important note is that using the fragment on the products query is not even required, as long as you select at least the same fields that the fragment has. The types are enforced at the field level, not fragment level. So you could query products { id name url availability price } and it would work the same.

If you forget to select any field on the query, TypeScript will complain, e.g. if you forgot availability:

Type '{ id: string; name: string; url: string; price: number; }' is missing the following properties from type 'ProductCardFieldsFragment': availability.

Why colocated fragments matter

This pattern gives you very practical advantages:

1. Avoiding type mismatches in real queries

Most page queries don’t select all fields of an entity. If your component’s props are typed as the entire Product, you’ll often end up with a type mismatch and resort to unsafe casts:

<ProductCard product={p as Product} /> // 👎 unsafe

In that case, if we forget to select a field in our query that the component needs, TypeScript won’t catch it and we’ll only know at runtime.

With the fragment approach, you never need to cast - the types line up automatically.

2. Better testing and Storybook ergonomics

Since your component only requires the fields in its fragment, you can build test objects or Storybook mocks with just those fields. No need to provide irrelevant Product fields.

render(
  <ProductCard
    product={{
      id: "1",
      name: "Coffee Mug",
      url: "/coffee",
      availability: "IN_STOCK",
      price: 12.95,
    }}
  />
);

3. Clearer component contracts

The fragment documents the component’s data needs in the same place where the component lives. That makes the component easier to reason about and reuse.


With a handful of tools - Apollo Client, GraphQL Code Generator, and a tiny utility type - you can achieve end-to-end type safety. Each component declares its own data needs with a fragment, and the page queries compose those fragments.

This pattern keeps your codebase ergonomic, self-documenting, and safe from “type mismatch” headaches.