TypeScript with GraphQL
GraphQL and TypeScript are a perfect match. GraphQL's schema-first approach provides automatic type safety, while TypeScript ensures your queries and resolvers are type-safe. In this lesson, we'll explore typed queries, code generation, typed resolvers, and practical GraphQL integration patterns.
Why GraphQL + TypeScript?
GraphQL and TypeScript complement each other perfectly:
- Schema-Driven Types: GraphQL schemas generate TypeScript types automatically
- End-to-End Safety: Types flow from schema to resolvers to client queries
- Auto-Completion: IDEs provide GraphQL query suggestions based on schema
- Refactoring Confidence: Schema changes surface type errors immediately
- Documentation: Types serve as living documentation
GraphQL Basics
First, let's understand GraphQL fundamentals:
# GraphQL Schema (schema.graphql)
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: DateTime
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
input UpdatePostInput {
title: String
content: String
}
GraphQL Code Generation
Use graphql-codegen to generate TypeScript types from your schema:
// Install dependencies
npm install --save-dev @graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-resolvers
// codegen.yml
schema: "./schema.graphql"
documents: "./src/**/*.graphql"
generates:
./src/generated/types.ts:
plugins:
- typescript
- typescript-operations
- typescript-resolvers
config:
useIndexSignature: true
enumsAsTypes: true
avoidOptionals: false
maybeValue: T | null
This generates comprehensive TypeScript types:
// generated/types.ts (excerpt)
export type User = {
__typename?: 'User';
id: Scalars['ID'];
name: Scalars['String'];
email: Scalars['String'];
posts: Array<Post>;
};
export type Post = {
__typename?: 'Post';
id: Scalars['ID'];
title: Scalars['String'];
content: Scalars['String'];
author: User;
publishedAt?: Maybe<Scalars['DateTime']>;
};
export type QueryUserArgs = {
id: Scalars['ID'];
};
export type MutationCreatePostArgs = {
input: CreatePostInput;
};
Typed GraphQL Resolvers
Create type-safe resolvers using generated types:
import { Resolvers, User, Post } from './generated/types';
// Database models (example)
interface UserModel {
id: string;
name: string;
email: string;
}
interface PostModel {
id: string;
title: string;
content: string;
authorId: string;
publishedAt: Date | null;
}
// Context type
interface Context {
db: {
users: UserModel[];
posts: PostModel[];
};
userId?: string;
}
// Typed resolvers
const resolvers: Resolvers<Context> = {
Query: {
user: async (_, { id }, context) => {
const user = context.db.users.find(u => u.id === id);
if (!user) return null;
return user;
},
users: async (_, __, context) => {
return context.db.users;
},
post: async (_, { id }, context) => {
const post = context.db.posts.find(p => p.id === id);
if (!post) return null;
return post;
}
},
Mutation: {
createPost: async (_, { input }, context) => {
if (!context.userId) {
throw new Error('Not authenticated');
}
const post: PostModel = {
id: generateId(),
title: input.title,
content: input.content,
authorId: input.authorId,
publishedAt: new Date()
};
context.db.posts.push(post);
return post;
}
},
User: {
posts: async (user, _, context) => {
return context.db.posts.filter(p => p.authorId === user.id);
}
},
Post: {
author: async (post, _, context) => {
const author = context.db.users.find(u => u.id === post.authorId);
if (!author) throw new Error('Author not found');
return author;
}
}
};
Note: The generated Resolvers type ensures all resolvers match the schema, with correct argument types, return types, and context.
Type-Safe GraphQL Client (graphql-request)
Use graphql-request for typed client queries:
npm install graphql graphql-request
import { GraphQLClient, gql } from 'graphql-request';
import { User, Post, CreatePostInput } from './generated/types';
const client = new GraphQLClient('http://localhost:4000/graphql', {
headers: {
authorization: 'Bearer token'
}
});
// Typed query
async function getUser(id: string): Promise<User | null> {
const query = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
publishedAt
}
}
}
`;
const data = await client.request<{ user: User | null }>(query, { id });
return data.user;
}
// Typed mutation
async function createPost(input: CreatePostInput): Promise<Post> {
const mutation = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
author {
id
name
}
publishedAt
}
}
`;
const data = await client.request<{ createPost: Post }>(mutation, { input });
return data.createPost;
}
// Usage
async function demo() {
const user = await getUser('123');
if (user) {
console.log(`User: ${user.name}`);
console.log(`Posts: ${user.posts.length}`);
}
const newPost = await createPost({
title: 'TypeScript + GraphQL',
content: 'Amazing combination!',
authorId: '123'
});
console.log(`Created post: ${newPost.title}`);
}
Advanced: Typed GraphQL Operations
Generate types for specific operations using graphql-codegen:
// queries.graphql
query GetUserWithPosts($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
content
publishedAt
}
}
}
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
author {
id
name
}
}
}
// codegen.yml (enhanced)
generates:
./src/generated/types.ts:
plugins:
- typescript
- typescript-operations
config:
namingConvention:
typeNames: pascal-case
enumValues: upper-case
This generates operation-specific types:
// generated/types.ts (additional)
export type GetUserWithPostsQuery = {
__typename?: 'Query';
user?: {
__typename?: 'User';
id: string;
name: string;
email: string;
posts: Array<{
__typename?: 'Post';
id: string;
title: string;
content: string;
publishedAt?: string | null;
}>;
} | null;
};
export type GetUserWithPostsQueryVariables = {
id: string;
};
export type CreatePostMutation = {
__typename?: 'Mutation';
createPost: {
__typename?: 'Post';
id: string;
title: string;
content: string;
author: {
__typename?: 'User';
id: string;
name: string;
};
};
};
export type CreatePostMutationVariables = {
input: CreatePostInput;
};
// Usage with operation types
import { GetUserWithPostsQuery, GetUserWithPostsQueryVariables } from './generated/types';
async function getUserWithPosts(id: string): Promise<GetUserWithPostsQuery['user']> {
const query = gql`
query GetUserWithPosts($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
content
publishedAt
}
}
}
`;
const variables: GetUserWithPostsQueryVariables = { id };
const data = await client.request<GetUserWithPostsQuery>(query, variables);
return data.user;
}
TypeScript + Apollo Server
Use Apollo Server with TypeScript for a complete type-safe GraphQL API:
npm install apollo-server graphql
npm install --save-dev @graphql-tools/schema
import { ApolloServer } from 'apollo-server';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { readFileSync } from 'fs';
import { Resolvers } from './generated/types';
// Load schema
const typeDefs = readFileSync('./schema.graphql', 'utf-8');
// Type-safe resolvers
const resolvers: Resolvers = {
Query: {
user: async (_, { id }, context) => {
return context.dataSources.users.findById(id);
}
}
};
// Create schema
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
// Context type
interface Context {
dataSources: {
users: UserDataSource;
posts: PostDataSource;
};
userId?: string;
}
// Create server
const server = new ApolloServer({
schema,
context: ({ req }): Context => ({
dataSources: {
users: new UserDataSource(),
posts: new PostDataSource()
},
userId: extractUserId(req.headers.authorization)
})
});
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
Error Handling
import { GraphQLError } from 'graphql';
const resolvers: Resolvers = {
Query: {
user: async (_, { id }, context) => {
const user = await context.dataSources.users.findById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: {
code: 'USER_NOT_FOUND',
userId: id
}
});
}
return user;
}
},
Mutation: {
createPost: async (_, { input }, context) => {
if (!context.userId) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
try {
return await context.dataSources.posts.create(input);
} catch (error) {
throw new GraphQLError('Failed to create post', {
extensions: {
code: 'CREATE_POST_FAILED',
originalError: error
}
});
}
}
}
};
Tip: Use GraphQL error extensions to provide structured error information with proper typing.
Subscription Support
// schema.graphql
type Subscription {
postCreated: Post!
userUpdated(userId: ID!): User!
}
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers: Resolvers = {
Mutation: {
createPost: async (_, { input }, context) => {
const post = await context.dataSources.posts.create(input);
// Publish subscription event
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
userUpdated: {
subscribe: (_, { userId }) => {
return pubsub.asyncIterator([`USER_UPDATED_${userId}`]);
}
}
}
};
Exercise: Create a GraphQL API with TypeScript:
- Define a schema for a blog (posts, comments, authors)
- Generate TypeScript types using graphql-codegen
- Implement typed resolvers with Apollo Server
- Create a typed client using graphql-request
- Add subscription support for real-time updates
- Implement proper error handling with custom error codes
Summary
In this lesson, you learned how to combine TypeScript with GraphQL for end-to-end type safety. We covered schema-based type generation, typed resolvers, type-safe client queries, and error handling. This combination provides unparalleled developer experience and confidence in your GraphQL APIs.