TL;DR
GraphQL schemas are strongly typed, and you can automatically generate TypeScript interfaces, enums, and input types from them. This eliminates manual type duplication, prevents runtime errors from schema drift, and keeps your frontend code fully type-safe. Try our free GraphQL to TypeScript converter for instant one-click conversion, or set up @graphql-codegen for automated pipeline integration.
Key Takeaways
- GraphQL type systems map cleanly to TypeScript interfaces, enums, and union types.
- Automated code generation eliminates type drift between your API schema and frontend code.
- The
@graphql-codegentoolchain supports typed hooks for React Query, Apollo, and urql. - Nullable fields in GraphQL translate to
T | nullunion types in TypeScript. - Custom scalars like
DateTimeandJSONneed explicit type mappings in your codegen config. - GraphQL provides inherently stronger typing than REST, reducing the need for manual interface definitions.
Why Generate TypeScript from GraphQL
GraphQL APIs define a complete type system in the schema. Every field, argument, and return type is explicitly declared. When you build a frontend that consumes a GraphQL API, you need TypeScript types that match the schema exactly. Writing these types by hand creates two sources of truth that inevitably diverge.
Consider a typical scenario: your backend team adds a new required field to a User type. Without automated type generation, your handwritten TypeScript interface stays unchanged. The mismatch only surfaces as a runtime error in production when the field is undefined where the code expected a string.
Automated GraphQL to TypeScript code generation solves this by treating the schema as the single source of truth. When the schema changes, you regenerate types, and the TypeScript compiler immediately flags every location in your frontend that needs updating. This workflow typically saves 2 to 5 hours per week on medium-sized projects and eliminates an entire class of bugs.
If you need a quick conversion without setting up a full pipeline, our GraphQL to TypeScript converter handles it instantly in the browser.
GraphQL Type System Overview
Before diving into code generation, you need to understand the GraphQL Schema Definition Language (SDL) and its type constructs. GraphQL supports six primary type categories:
- Object types (
type) - define the shape of data objects with named fields - Input types (
input) - define argument shapes for mutations and queries - Enum types (
enum) - define a fixed set of allowed values - Interface types (
interface) - abstract types that other types implement - Union types (
union) - represent one of several possible object types - Scalar types (
scalar) - primitive values likeString,Int,Boolean,Float,ID, or custom scalars
The exclamation mark (!) denotes a non-nullable field. Without it, the field defaults to nullable. This is the opposite of most programming languages and is critical to understand when mapping to TypeScript. Here is a schema demonstrating all major constructs:
# Object type with various field types
type User {
id: ID!
name: String!
email: String!
age: Int
bio: String
role: UserRole!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
status: PostStatus!
author: User!
tags: [String!]
publishedAt: DateTime
}
# Enum types
enum UserRole {
ADMIN
EDITOR
VIEWER
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
# Input type for mutations
input CreateUserInput {
name: String!
email: String!
age: Int
role: UserRole = VIEWER
}
# Union type
union SearchResult = User | Post
# Custom scalar
scalar DateTimeManual Conversion Examples
Understanding manual conversion helps you validate generated output and debug codegen issues. Below are the four most common GraphQL-to-TypeScript mappings you will encounter.
Object Type to Interface
GraphQL object types map to TypeScript interfaces. Non-nullable fields (!) become required properties, and nullable fields become union types with null:
// From GraphQL: type User { id: ID!, name: String!, age: Int, bio: String }
interface User {
id: string;
name: string;
age: number | null;
bio: string | null;
}Input Type to Interface
Input types follow the same mapping rules. Fields with default values in GraphQL become optional in TypeScript:
// From GraphQL: input CreateUserInput { name: String!, email: String!, age: Int, role: UserRole = VIEWER }
interface CreateUserInput {
name: string;
email: string;
age?: number | null;
role?: UserRole;
}Enum to TypeScript Enum or Union
GraphQL enums can map to either TypeScript enums or string literal unions. Both approaches are valid, but string literal unions are often preferred for tree-shaking:
// Approach 1: TypeScript enum
enum UserRole {
ADMIN = 'ADMIN',
EDITOR = 'EDITOR',
VIEWER = 'VIEWER',
}
// Approach 2: String literal union (preferred for tree-shaking)
type UserRole = 'ADMIN' | 'EDITOR' | 'VIEWER';Union Type to Discriminated Union
GraphQL union types become TypeScript discriminated unions. The __typename field acts as the discriminator:
// From GraphQL: union SearchResult = User | Post
type SearchResult =
| { __typename: 'User'; id: string; name: string; email: string }
| { __typename: 'Post'; id: string; title: string; content: string };
// Usage with type narrowing
function renderResult(result: SearchResult) {
switch (result.__typename) {
case 'User':
return result.name; // TypeScript knows this is User
case 'Post':
return result.title; // TypeScript knows this is Post
}
}For more on TypeScript generics and advanced type patterns, see our guide on TypeScript generics explained.
Using the Online Converter Tool
For quick prototyping, learning, or one-off conversions, you do not need a full codegen pipeline. Our GraphQL to TypeScript converter provides instant browser-based conversion. Here is a typical workflow:
- Paste your GraphQL SDL into the input panel. The tool accepts full schemas with multiple types, enums, inputs, and unions.
- View the generated TypeScript in the output panel. Types are generated with proper nullability, array handling, and enum mapping.
- Copy the output directly into your project. Use the one-click copy button to grab the generated code.
The online tool handles all standard GraphQL constructs including nested types, recursive references, and list fields. It is ideal when you want to:
- Quickly check how a schema change affects your TypeScript types
- Generate initial type definitions for a new project before setting up codegen
- Learn the mapping rules by comparing GraphQL input to TypeScript output
- Share type definitions with team members who do not have codegen configured
If you also work with JSON data, check out our JSON to TypeScript converter and JSON to GraphQL converter for related transformations.
graphql-codegen Setup Guide
For production projects, @graphql-codegen is the industry standard. It reads your GraphQL schema and operations, then generates TypeScript types, typed document nodes, and even framework-specific hooks. Here is a complete setup:
Step 1: Install Dependencies
# Core packages
npm install -D @graphql-codegen/cli @graphql-codegen/typescript
npm install -D @graphql-codegen/typescript-operations
npm install -D @graphql-codegen/typescript-resolvers
# Optional: framework-specific plugins
npm install -D @graphql-codegen/typescript-react-query # for React Query
npm install -D @graphql-codegen/typescript-react-apollo # for Apollo ClientStep 2: Create Configuration File
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
// Schema source: local file or remote endpoint
schema: './schema.graphql',
// Or use introspection: schema: 'http://localhost:4000/graphql',
// Where your .graphql operation files live
documents: ['src/**/*.graphql'],
generates: {
// Generated types output path
'src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
],
config: {
// Use string literal unions instead of TS enums
enumsAsTypes: true,
// Make nullable fields T | null instead of Maybe<T>
strictScalars: true,
// Map custom scalars
scalars: {
DateTime: 'string',
JSON: 'Record<string, unknown>',
Upload: 'File',
},
},
},
},
};
export default config;Step 3: Add npm Scripts
// package.json
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch",
"dev": "concurrently \"next dev\" \"npm run codegen:watch\"",
"precommit": "npm run codegen && tsc --noEmit"
}
}Step 4: Run Generation
# One-time generation
npm run codegen
# Watch mode during development
npm run codegen:watchAfter running codegen, all your TypeScript types are available in src/generated/graphql.ts. Import them directly in your components and hooks.
Generated Types with React Query and Apollo
The real power of GraphQL code generation emerges when you generate typed hooks for your data-fetching library. Instead of manually typing query results, codegen produces hooks where the return data is already fully typed.
With React Query (TanStack Query)
Add the typescript-react-query plugin to your codegen config:
// codegen.ts - add to generates section
'src/generated/hooks.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-query',
],
config: {
fetcher: {
func: './fetcher#fetchGraphQL',
isReactHook: false,
},
exposeQueryKeys: true,
},
}
// Usage in a component - fully typed, zero manual interfaces
import { useGetUserQuery } from '../generated/hooks';
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useGetUserQuery({ id: userId });
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error occurred</div>;
// data.user is fully typed: { id: string, name: string, ... }
return <h1>{data?.user?.name}</h1>;
}With Apollo Client
// codegen.ts - Apollo plugin
'src/generated/apollo.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: {
withHooks: true,
withComponent: false,
},
}
// Usage - Apollo hooks are fully typed
import { useGetUserQuery } from '../generated/apollo';
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id: userId },
});
// data?.user is fully typed from the schema
return <h1>{data?.user?.name}</h1>;
}Both approaches give you autocomplete in your editor, compile-time error checking, and zero chance of type drift. When the schema changes, regenerate types and fix any compile errors before deploying.
Handling Nullable Types and Custom Scalars
Two areas commonly cause confusion when generating TypeScript from GraphQL: nullability and custom scalars. Getting these right is essential for accurate type generation.
Nullable Field Mapping
In GraphQL, fields are nullable by default. The ! makes them non-nullable. This is the opposite of TypeScript where properties are non-nullable by default. Here is the complete mapping table:
| GraphQL Type | TypeScript Type | Explanation |
|---|---|---|
String | string | null | Nullable string |
String! | string | Non-nullable string |
[String] | (string | null)[] | null | Nullable list of nullable strings |
[String!] | string[] | null | Nullable list of non-nullable strings |
[String!]! | string[] | Non-nullable list of non-nullable strings |
[String]! | (string | null)[] | Non-nullable list of nullable strings |
Custom Scalar Configuration
GraphQL supports custom scalars beyond the built-in String, Int, Float, Boolean, and ID. These need explicit TypeScript mappings in your codegen config:
// codegen.ts - scalars config section
config: {
scalars: {
// Date/time scalars
DateTime: 'string', // ISO 8601 date strings
Date: 'string', // YYYY-MM-DD format
Time: 'string', // HH:mm:ss format
Timestamp: 'number', // Unix timestamp
// Data scalars
JSON: 'Record<string, unknown>',
JSONObject: 'Record<string, unknown>',
// File uploads
Upload: 'File',
// Numeric precision
BigInt: 'bigint',
Decimal: 'string', // Preserve precision as string
// Other common scalars
UUID: 'string',
URL: 'string',
EmailAddress: 'string',
PhoneNumber: 'string',
Void: 'void',
},
}For more advanced typing patterns like branded types for custom scalars, see our TypeScript generics guide.
Best Practices for Type-Safe GraphQL
Generating types is only the first step. Follow these best practices to maintain a robust, type-safe GraphQL workflow across your entire team:
- Never manually edit generated files. Add a comment header to generated files and configure your linter to skip them. If you need custom types, create a separate file that imports and extends the generated ones.
- Run codegen in watch mode during development. This keeps types updated automatically as you modify
.graphqlfiles, giving you instant feedback in your editor. - Add codegen to your CI pipeline. Run
npm run codegenfollowed bytsc --noEmitin CI to catch schema drift before code reaches production. - Commit generated files to version control. This makes pull request diffs clearly show how schema changes affect your frontend types and lets developers work without running codegen locally.
- Use fragment colocation. Define GraphQL fragments alongside the components that consume them. This keeps data requirements close to the UI code and produces more focused generated types.
- Prefer string literal unions over TypeScript enums. Configure
enumsAsTypes: truein your codegen config. String unions tree-shake better, have simpler runtime behavior, and work more naturally with discriminated unions. - Enable strict mode. Set
strictScalars: trueto force explicit scalar mappings andavoidOptionals: trueto useT | nullinstead of optional properties. This catches more errors at compile time. - Use typed document nodes. The
typed-document-nodeplugin generates typed query/mutation objects that work with any GraphQL client, providing type safety without framework-specific plugins.
GraphQL vs REST Typing Comparison
Both GraphQL and REST APIs can be consumed with full type safety in TypeScript, but they achieve it through very different mechanisms. Understanding the differences helps you choose the right approach for your project.
| Aspect | GraphQL + Codegen | REST + OpenAPI |
|---|---|---|
| Schema source | GraphQL SDL (built into protocol) | OpenAPI/Swagger spec (external document) |
| Type generation | @graphql-codegen | openapi-typescript or swagger-typescript-api |
| Response shape | Client-defined (query determines fields) | Server-defined (fixed per endpoint) |
| Type precision | Exact types per query (no over-fetching) | Full response type (may include unused fields) |
| Enum support | Native (part of SDL) | Via OpenAPI enum keyword |
| Nullability | Explicit per-field (! annotation) | Via required and nullable keywords |
| Typed hooks | Generated per query/mutation | Manual or via libraries like orval |
| Schema drift detection | Compile-time via codegen + tsc | Compile-time via generated types + tsc |
GraphQL has an inherent advantage in type precision because the client specifies exactly which fields it needs, and codegen generates types matching only those fields. REST types always represent the full response object, even when the client only uses a subset.
For a deeper comparison of these API paradigms, read our REST API best practices guide.
Frequently Asked Questions
What is the best tool for GraphQL to TypeScript code generation?
@graphql-codegen (GraphQL Code Generator) is the most widely adopted solution. It is plugin-based, supports every major GraphQL client (Apollo, urql, React Query), and generates not just types but also typed hooks, document nodes, and resolvers. For quick one-off conversions without toolchain setup, use our online GraphQL to TypeScript converter.
How do nullable GraphQL fields map to TypeScript types?
GraphQL fields are nullable by default (the opposite of TypeScript). A field without ! maps to T | null in TypeScript. A field with ! maps directly to T. Lists follow the same rules independently for the list itself and its elements. For example, [String!] becomes string[] | null (nullable list, non-nullable items).
Can I generate TypeScript types from a remote GraphQL endpoint?
Yes. Most code generators support introspection, meaning they query the running GraphQL server for its schema. In your codegen config, set the schema field to your endpoint URL (e.g., http://localhost:4000/graphql). For production APIs, you can also export the schema as an SDL file and use that as the source to avoid requiring a running server during builds.
Should I commit generated TypeScript files to version control?
Yes. Committing generated files provides two benefits: (1) pull request diffs clearly show how backend schema changes affect frontend types, and (2) developers can work on frontend code without needing to run the codegen pipeline locally. The key rule is to never manually edit generated files. If you need to extend a type, create a separate file that imports from the generated output.
What is the difference between graphql-codegen and gql.tada?
graphql-codegen generates TypeScript files at build time from your schema. gql.tada takes a different approach: it uses TypeScript template literal types to infer types at compile time without generating any files. graphql-codegen has broader ecosystem support and works with any GraphQL client. gql.tada offers zero-config type safety but requires TypeScript 5.0+ and advanced TypeScript understanding. For most teams, graphql-codegen is the safer and more practical choice.
Related Tools and Guides
- GraphQL to TypeScript Converter - instant browser-based schema conversion
- JSON to TypeScript Converter - generate interfaces from JSON data
- JSON to GraphQL Converter - create GraphQL schemas from JSON objects
- TypeScript Generics Explained - master advanced TypeScript type patterns
- REST API Best Practices Guide - comprehensive REST API design patterns