GraphQL schemas are just text files, but imagine if you could treat them like the bedrock of your application’s data contract.
Let’s see this in action. We have a simple GraphQL schema:
# schema.graphql
type User {
id: ID!
name: String!
email: String
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
type Query {
user(id: ID!): User
posts: [Post!]!
}
Now, we want to generate TypeScript types that perfectly mirror this structure. We can use a tool like graphql-codegen for this. First, install it and its necessary plugins:
npm install --save-dev graphql graphql-codegen @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
Next, create a configuration file, codegen.yml:
# codegen.yml
schema: ./schema.graphql
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
config:
# Optional: if you want to generate types for queries/mutations/subscriptions
# automatically when using `typescript-operations`
enumsAsTypes: true
# Use `identity` for basic types, or `react-query` or `apollo` for specific client integrations
# For this example, we'll stick to basic types.
# gqlCodegen: identity
With this setup, running npx graphql-codegen --config codegen.yml will create a src/generated/graphql.ts file. Inside, you’ll find types like these:
// src/generated/graphql.ts (simplified)
export type Maybe<T> = T | null;
export type InputMaybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [P in K]?: T[P] };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [P in K]: Maybe<T[P]> };
export type MakePlain<T, K extends keyof T> = Omit<T, K> & { [P in K]: T[P] };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]: never };
export type Incremental<T> = T | { [P in keyof T]?: T[P] };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
};
export type Post = {
__typename?: 'Post';
author: User;
content?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
title: Scalars['String']['output'];
};
export type User = {
__typename?: 'User';
email?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
posts: Array<Post>;
};
export type Query = {
__typename?: 'Query';
posts: Array<Post>;
user?: Maybe<User>;
};
export type QueryUserArgs = {
id: Scalars['ID']['input'];
};
This generated code is not just a collection of interfaces; it’s a fully-typed representation of your GraphQL API. The Maybe<T> and InputMaybe<T> types handle nullable fields, Array<Post> correctly represents lists, and the QueryUserArgs type defines the exact shape of arguments for your queries.
The problem this solves is the classic impedance mismatch between client-side code and a server’s API. Without generated types, you’re manually writing interfaces for every field, every argument, and every possible response shape. This is tedious, error-prone, and becomes a maintenance nightmare as your schema evolves. graphql-codegen automates this, ensuring your TypeScript code is always in sync with your GraphQL schema.
Internally, graphql-codegen parses your GraphQL schema file. It then traverses the Abstract Syntax Tree (AST) of the schema, identifying types, fields, arguments, and their respective types (scalar, object, list, non-null). Based on these findings and the configured plugins (like typescript and typescript-operations), it generates corresponding TypeScript code. The typescript-operations plugin specifically looks for graphql tagged template literals in your code (if you’re using a client like Apollo or Relay) and generates types for those queries, mutations, and subscriptions as well.
The config section in codegen.yml is where you wield the real power. For instance, setting enumsAsTypes: true transforms GraphQL enum types into distinct TypeScript type aliases, providing stronger type safety than simple string unions. If you were using react-query as a codegen preset, it would generate hook types like UseQueryOptions and UseQueryResult tailored for that library, inferring return types and argument types directly from your GraphQL queries.
One of the most powerful, yet often overlooked, aspects is how graphql-codegen handles __typename. By default, the generated types include a __typename?: 'TypeName' property. This might seem like boilerplate, but it’s crucial for discriminated unions when dealing with polymorphic fields (like interfaces or unions in GraphQL). It allows TypeScript to narrow down the type of an object based on its __typename, enabling precise type checking for different implementations of an interface or members of a union.
The next hurdle you’ll likely encounter is managing complex query fragments and ensuring they are correctly typed and reused across your application.