TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
66: Typing GraphQL Schemas and Resolvers
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
anytype from theresolversobject and useBookResolvers(or a combined type) to ensure theBookfield resolvers are complete. - Implement the
genreresolver inside theBookobject. It should use theparent.genreIdto find the correct genre from thegenresDataarray. - Ensure the
authorresolver is also properly typed using theBookinterface for theparentargument.
There are no comments for now.