13.4 GraphQL + TypeScript — Auto-generating Types with graphql-codegen
The Core Problem with GraphQL + TypeScript
Manually converting GraphQL schemas to TypeScript types is tedious and error-prone. graphql-code-generator automates this process.
GraphQL Schema (.graphql) → graphql-codegen → TypeScript types (.ts)
Installing and Configuring graphql-codegen
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers @graphql-codegen/typescript-operations
# codegen.yml
overwrite: true
schema: "src/schema/**/*.graphql"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
- typescript-operations
config:
strictScalars: true
scalars:
DateTime: string
JSON: Record<string, unknown>
// package.json
{
"scripts": {
"codegen": "graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch"
}
}
Server Side: Resolver Types
# schema/user.graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users(page: Int, limit: Int): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
email: String
}
// resolvers/user.resolver.ts
import { Resolvers } from '../generated/graphql'
import { UserService } from '../services/user.service'
const userService = new UserService()
export const userResolvers: Resolvers = {
Query: {
// id: string (auto-typed)
user: async (_, { id }) => {
return userService.findById(id)
},
users: async (_, { page, limit }) => {
return userService.findAll({ page: page ?? 1, limit: limit ?? 10 })
},
},
Mutation: {
createUser: async (_, { input }) => {
// input: CreateUserInput (auto-typed)
return userService.create(input)
},
updateUser: async (_, { id, input }) => {
return userService.update(id, input)
},
},
User: {
// Field resolver
posts: async (user) => {
return postService.findByUserId(user.id)
},
},
}
Apollo Server + TypeScript
// src/server.ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { readFileSync } from 'fs'
import { join } from 'path'
import { userResolvers } from './resolvers/user.resolver'
import { postResolvers } from './resolvers/post.resolver'
import type { Context } from './types/context'
// Load schema file
const typeDefs = readFileSync(join(__dirname, 'schema/schema.graphql'), 'utf-8')
// Context type
export interface Context {
user: AuthUser | null
db: DatabaseClient
dataSources: {
userAPI: UserAPI
}
}
const server = new ApolloServer<Context>({
typeDefs,
resolvers: [userResolvers, postResolvers],
})
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '')
const user = token ? await verifyToken(token) : null
return {
user,
db,
dataSources: { userAPI: new UserAPI() },
}
},
listen: { port: 4000 },
})
console.log(`🚀 GraphQL server: ${url}`)
Client Side: Query Types
# src/queries/user.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
createdAt
}
}
query GetUsers($page: Int, $limit: Int) {
users(page: $page, limit: $limit) {
id
name
email
}
}
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
// Using generated types (React + Apollo Client)
import {
useGetUserQuery,
useGetUsersQuery,
useCreateUserMutation,
GetUserQuery,
GetUserQueryVariables,
} from '../generated/graphql'
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id: userId }, // GetUserQueryVariables type
})
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
const user = data?.user // GetUserQuery['user'] type
if (!user) return <div>User not found</div>
return <div>{user.name} - {user.email}</div>
}
function CreateUserForm() {
const [createUser, { loading }] = useCreateUserMutation({
onCompleted: (data) => {
console.log('Created:', data.createUser.id) // Type-safe
},
})
const handleSubmit = async (name: string, email: string) => {
await createUser({
variables: { input: { name, email } }, // Type-safe
})
}
}
urql + TypeScript
Using urql instead of Apollo:
import { useQuery, useMutation } from 'urql'
import {
GetUserDocument,
CreateUserDocument,
GetUserQuery,
GetUserQueryVariables,
} from '../generated/graphql'
function UserComponent({ id }: { id: string }) {
const [{ data, fetching, error }] = useQuery<GetUserQuery, GetUserQueryVariables>({
query: GetUserDocument,
variables: { id },
})
return <div>{data?.user?.name}</div>
}
Pro Tips
1. Customizing scalar types
# codegen.yml
config:
scalars:
DateTime: Date # Use Date object instead of string
UUID: string
JSON: Record<string, unknown>
Upload: File
2. Type reuse with Fragments
fragment UserBasic on User {
id
name
email
}
query GetUser($id: ID!) {
user(id: $id) {
...UserBasic
createdAt
}
}
// Reuse generated Fragment types
import { UserBasicFragment } from '../generated/graphql'
function UserCard({ user }: { user: UserBasicFragment }) {
return <div>{user.name}</div>
}