GraphQL to TypeScript
Generate TypeScript types from GraphQL SDL — objects, inputs, enums, unions and operation result types — with nullability mapped exactly.
Paste a GraphQL schema on the left to generate TypeScript types.
Press / to jump to the schema pane, or load the example schema and query.
Press ? for shortcuts · Schema, operation and output stay on this device — nothing is uploaded. · Nothing is stored between visits.
Do it offline:$ npx graphql-codegen --config codegen.yml
How to use it
- Paste your schema (SDL) into the left pane; it is parsed on this device and nothing is uploaded.
- Optionally paste one or more operations into the pane below — the output switches from schema types to result and variables types.
- Press Generate, or ⌘/Ctrl + Enter. Changing any option regenerates immediately; editing an input marks the output stale until you generate again.
- Read the notes under the output: every custom scalar that fell back to the preset is named there, one note per scalar.
- Copy the TypeScript or download it as a .ts file. Share links carry the four generator options only — never your schema or operation.
GraphQL and TypeScript disagree about absence: a GraphQL field can return null unless the schema marks it with !, while TypeScript assumes a property is present and non-null unless the type says otherwise. This page reads pasted SDL and emits interfaces, enums, unions and scalar aliases with that gap mapped exactly — and given a query, it types the result and variables too.
Examples
type Track {
id: ID!
title: String!
rating: Float
tags: [String!]!
credits: [[String!]]
}export interface Track {
__typename?: "Track";
id: string;
title: string;
rating?: number | null;
tags: Array<string>;
credits?: Array<Array<string> | null> | null;
}
The two ! fields come out required and bare; rating, with no !, becomes an optional property that may also hold null — two different absences, both spelled out. Each list layer of credits keeps its own nullability, inside out.
# Schema pane (SDL)
type Query {
album(id: ID!): Album
}
type Album {
id: ID!
name: String!
year: Int
}
# Operation pane
query AlbumCard {
album(id: "al_1") {
...AlbumCore
year
}
}
fragment AlbumCore on Album {
id
name
}export interface AlbumCardQuery {
album?: {
id: string;
name: string;
year?: number | null;
} | null;
}
Each labeled half goes in its pane. An operation types what it selects, not the whole schema. The fragment merges at its spread position, so id and name sit before year, and the shape is inline — no named Album interface is emitted.
# Schema pane (SDL)
type Query {
search(term: String!, limit: Int, exact: Boolean = false): [String!]!
}
# Operation pane
query Search($term: String!, $limit: Int, $exact: Boolean = false) {
search(term: $term, limit: $limit, exact: $exact)
}export interface SearchQueryVariables {
term: string;
limit?: number | null;
exact?: boolean | null;
}
export interface SearchQuery {
search: Array<string>;
}
Paste each half into its labeled pane. Variables get their own interface. $term is non-null with no default, so it is required; $limit may be omitted or null; $exact carries a default, so callers may omit it though the server sees a value.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Null is the default, not the exception
Every GraphQL output field is nullable until the schema says !, which is backwards from how most TypeScript is written. The generator refuses to paper over that: unmarked fields render as `| null`, and the default style adds ? so the property may also be absent. Switch the nullable option to `field: T | null` when your codebase bans optional properties.
Every list layer wraps its own null
[Int] is a nullable list of nullable numbers — Array<number | null> | null. [Int]! pins the list, [Int!] pins the elements, [Int!]! pins both to Array<number>, and [[Int!]]! maps to Array<Array<number> | null>. The wrapping is mechanical, inside out, which is exactly why hand-written types drift.
Custom scalars have no shape of their own
The spec fixes ID, String, Int, Float and Boolean; a scalar DateTime says nothing about its wire format. Each custom scalar becomes an exported alias to the preset you chose — unknown by default, string or Record<string, unknown> if you switch — and a note names every scalar that was defaulted this way.
@skip and @include make fields absent, not null
A field behind either directive may be missing from the response even when its type is non-null, so it renders with ? and no added `| null` — email: String! under @include becomes email?: string. Absence and null stay two different facts in the emitted types, matching what servers return.
Where the limits sit
The schema pane takes 1 MiB — the shared GraphQL parser's bound — and the operation pane 256 KiB; generated output is capped at 4 MiB, with deeply spliced fragments refused rather than expanded forever. Every refusal names its limit and nothing is truncated.
Common errors
Expected a field definition or "}", found end of input.- The schema ended inside a type block. The parser reports the position where the closing brace should be, and your input stays put while you fix it.
Unknown type "Trak" — referenced here but never defined in this schema.- Every named reference must resolve before anything is emitted — a typo would otherwise become a TypeScript type that compiles against nothing. The caret points at the reference itself.
An operation alone cannot be typed — paste the schema (SDL) it runs against, and the result types follow.- Selections only have types relative to a schema. There is no guessing mode: without the SDL, the honest answer is a refusal, not a stringly-typed shrug.
Field "coverUrl" does not exist on type "Album".- The selection walk checks every field against the schema before emitting anything, the same check a server would run. Aliases, fragments and nested selections are validated the same way.
FAQ
Is this the same thing as graphql-codegen?
No — GraphQL Code Generator is the ecosystem tool for that job at CI scale: watch mode, plugin pipelines, resolver and client typings across many files. This page is the paste-and-read version of its core mapping for one schema and one operation document, with the same nullability law. When you outgrow it, the command below is the hand-off.
Why is a field both optional and | null?
Because a response can express two different absences: a key that is missing and a key that is present holding null. GraphQL nullability covers the null; @skip and @include cover the missing key. The default style writes both facts down, and the nullable-only style collapses to `field: T | null` when your team prefers required properties.
Can this fetch my schema from an endpoint?
No. Nothing on this page calls out — there is no URL field, no introspection request, and generation runs entirely on this device. Paste SDL from your repo or from your server's printed schema. Accepting an introspection JSON file is a recorded follow-up, not a hidden feature.
Keyboard shortcuts
- /
- Focus the schema pane.
- ⌘/Ctrl + Enter
- Generate TypeScript from the current panes.
- ⌘/Ctrl + S
- Download the generated .ts file.
- ⌘/Ctrl + C
- Copy the TypeScript when nothing is selected.
- ⌘/Ctrl + Shift + C
- Copy the TypeScript despite a selection.
- t
- Cycle to the next theme.
- ? or ⌘/Ctrl + /
- Show the keyboard shortcuts.
- Esc
- Close the topmost dialog; both panes keep their text.