TypeScript

TypeScript with GraphQL

25 min Lesson 36 of 40

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:
  1. Define a schema for a blog (posts, comments, authors)
  2. Generate TypeScript types using graphql-codegen
  3. Implement typed resolvers with Apollo Server
  4. Create a typed client using graphql-request
  5. Add subscription support for real-time updates
  6. 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.