TypeScript can infer types directly from your GraphQL schema, saving you from manually writing and maintaining type definitions for your API.

Let’s see this in action. Imagine you have a simple GraphQL schema:

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

type Query {
  user(id: ID!): User
  users: [User!]!
}

And you’re using Apollo Server to serve this. In your frontend (or even backend) TypeScript code, you want to fetch a user and be sure about the types.

Here’s how you’d typically fetch data in your TypeScript application:

import { gql, useQuery } from '@apollo/client';

// This is the GraphQL query itself
const GET_USER_QUERY = gql`
  query GetUser($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }
`;

interface GetUserQueryData {
  user: {
    id: string;
    name: string;
    email: string | null;
  } | null;
}

interface GetUserQueryVariables {
  userId: string;
}

function UserProfile({ userId }: { userId: string }) {
  const { loading, error, data } = useQuery<GetUserQueryData, GetUserQueryVariables>(GET_USER_QUERY, {
    variables: { userId },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  const user = data?.user;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>Email: {user?.email ?? 'Not provided'}</p>
    </div>
  );
}

Notice how GetUserQueryData and GetUserQueryVariables are manually defined. This is where the magic of schema generation comes in. Tools like graphql-codegen can read your schema.graphql file and automatically produce these types.

The core problem this solves is the disconnect between your GraphQL API’s contract (the schema) and your application code’s understanding of that contract. Without generated types, you’re essentially writing the same information twice: once in your schema and once in your TypeScript interfaces. This leads to:

  1. Drift: The schema changes, but the TypeScript types don’t, leading to runtime errors.
  2. Boilerplate: Manually writing and updating types is tedious and error-prone.
  3. Reduced Confidence: You can’t be 100% sure your code’s types match the actual API responses.

graphql-codegen works by parsing your GraphQL schema and applying various "plugins" to generate different kinds of output. For TypeScript, the most common plugins are:

  • @graphql-codegen/typescript: Generates basic TypeScript types for your schema (scalars, objects, enums, input objects, unions, interfaces).
  • @graphql-codegen/typescript-operations: Generates types for your GraphQL operations (queries, mutations, subscriptions), including types for variables and the returned data.
  • @graphql-codegen/typescript-react-apollo (or similar for other frameworks): Generates higher-level hooks and components (like useQuery, useMutation) with inferred types, so you don’t even need to define the gql tag or the data/variable interfaces yourself.

Here’s a typical codegen.yml configuration:

# codegen.yml
schema: ./schema.graphql
documents: ./src/**/*.{ts,tsx}
generates:
  ./src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo # Or typescript-apollo-angular, etc.
    config:
      skipTypename: true # Useful if you don't need __typename
      withHooks: true # Generates hooks like useQuery

Running npx graphql-codegen --config codegen.yml will then create a src/generated/graphql.ts file containing all the necessary types. Your UserProfile component would then look much simpler:

import { gql, useQuery } from '@apollo/client';
import { GetUserQuery, GetUserQueryVariables } from './generated/graphql'; // Imported types

// The query string might also be generated, or you'd reference it.
// For simplicity, let's assume it's still here for now.
const GET_USER_QUERY = gql`
  query GetUser($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }
`;

function UserProfile({ userId }: { userId: string }) {
  // Use the generated hook, which already knows the types
  const { loading, error, data } = useQuery<GetUserQuery, GetUserQueryVariables>(GET_USER_QUERY, {
    variables: { userId },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  // 'data' is now typed as GetUserQuery
  const user = data?.user;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>Email: {user?.email ?? 'Not provided'}</p>
    </div>
  );
}

The graphql-codegen tool doesn’t just mirror your schema; it can also perform transformations. For instance, it can automatically generate enum types in TypeScript from your GraphQL enum definitions, or map GraphQL Date scalars to JavaScript Date objects. It also handles the nuances of nullability (String vs. String!) and array wrappers ([User!]! vs. [User]!) correctly, ensuring your TypeScript types accurately reflect the GraphQL schema’s constraints.

The most powerful aspect is how it integrates with your existing GraphQL client. When using typescript-react-apollo, you don’t even write the gql tagged template literal yourself; codegen can often generate it for you based on the documents glob in your config. This means your entire GraphQL interaction layer – from fetching to type checking – is fully automated and derived directly from your single source of truth: the GraphQL schema.

The next logical step is to automate the generation of your GraphQL server resolvers based on the same schema.

Want structured learning?

Take the full Graphql-tools course →