Skip to Content
Course content

66: Typing GraphQL Schemas and Resolvers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

So, we've got our GraphQL server running, and the schema looks great in the playground. But if you look at the actual TypeScript code for the resolvers, you'll probably notice something depressing: it's basically just JavaScript with a few any types thrown in. That's where most people stop, but it's a huge missed opportunity for safety.

The "Just Make it Work" Phase

Let's look at a simple scenario. I'm building a small system for a bookstore. I have a Book and an Author. Here is my current resolver setup:

const resolvers = {
  Query: {
    book: (parent, args, context) => {
      return books.find(b => b.id === args.id);
    }
  },
  Book: {
    author: (parent) => {
      // Who is 'parent'? TypeScript has no idea.
      return authors.find(a => a.id === parent.authorId);
    }
  }
};

Right now, parent is implicitly any. I'm just trusting that whatever the book query returns will have an authorId property. This works fine until I rename authorId to writerId in my database but forget to update this specific resolver. The app crashes at runtime, and TypeScript didn't say a word. That's not why we're using TypeScript, right?

Fighting the Maintenance Monster

My first instinct—and what I did early in my career—was to just manually write interfaces that match the GraphQL schema.

interface Book {
  id: string;
  title: string;
  authorId: string;
}

interface Author {
  id: string;
  name: string;
}

// Now I can type the resolver
const bookResolvers = {
  author: (parent: Book): Author | null => {
    return authors.find(a => a.id === parent.authorId) || null;
  }
};

This feels better. I have autocomplete! I have type checking! But here's the problem: I now have two sources of truth. I have the GraphQL SDL (Schema Definition Language) and I have these TypeScript interfaces. If I add a genre field to the Book type in my schema, I have to remember to add it to the Book interface too. If I don't, my resolvers are lying to me about what the data looks like.

It's a maintenance nightmare waiting to happen. I don't want to write the same structure twice.

Closing the Loop with Generated Types

The real breakthrough happens when we stop writing the types by hand and start deriving them from the schema itself. In a professional environment, we use tools like GraphQL Code Generator, but the concept is what matters here: the schema should be the single source of truth.

Imagine we've run a generator that looks at our type Book { ... } in the schema and spits out a Book type in TypeScript. Now, look at how we can use those generated types to lock down our resolvers:

import { Resolvers } from './generated/graphql';

// By typing the whole object as 'Resolvers', 
// TS now knows exactly what 'parent' and 'args' are for every single field.
const resolvers: Resolvers = {
  Query: {
    book: (_, { id }) => {
      // 'id' is automatically typed as a string because the schema says so
      return books.find(b => b.id === id);
    }
  },
  Book: {
    author: (parent) => {
      // parent is now automatically typed as Book!
      // If I try to access parent.writerId, TS will throw an error.
      return authors.find(a => a.id === parent.authorId);
    }
  }
};

This is the "gold standard." By casting the resolvers object to a generated Resolvers type, TypeScript can map the GraphQL schema's requirements directly onto our function signatures. If I change a field name in the schema and regenerate the types, my IDE will immediately light up with red squiggles everywhere that resolver is now broken. That's the kind of confidence I want when I'm pushing to production.




📋 Practical Task

Exercise: Fixing the "Missing Genre" Resolver

You are working on a Library API. The GraphQL schema was recently updated to include a genre field on the Book type, and a new Genre type was created to provide more details (like a description).

Below is a resolver that is currently using any and is causing runtime errors because the developer forgot to implement the new genre field resolver. Your task is to replace the any types with the provided generated interfaces to catch the errors and implement the missing logic.

// Generated Types (Assume these are provided by your generator)
interface Book {
  id: string;
  title: string;
  genreId: string;
}

interface Genre {
  id: string;
  name: string;
  description: string;
}

interface BookResolvers {
  genre: (parent: Book) => Genre | null;
}

const genresData: Genre[] = [
  { id: '1', name: 'Sci-Fi', description: 'Speculative fiction' },
  { id: '2', name: 'Fantasy', description: 'Magic and mythical creatures' },
];

// CURRENT BROKEN IMPLEMENTATION
const resolvers: any = {
  Book: {
    // This is currently missing the 'genre' resolver entirely, 
    // and the developer used 'any' so TS didn't warn them.
    author: (parent: any) => {
      return authors.find(a => a.id === parent.authorId);
    }
  }
};

Requirements:

  • Remove the any type from the resolvers object and use BookResolvers (or a combined type) to ensure the Book field resolvers are complete.
  • Implement the genre resolver inside the Book object. It should use the parent.genreId to find the correct genre from the genresData array.
  • Ensure the author resolver is also properly typed using the Book interface for the parent argument.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.