Why a GraphQL schema and its TypeScript types drift apart
A GraphQL API and a TypeScript frontend describe the same data twice — once in the schema's SDL, and again in whatever interfaces a developer writes by hand to represent a response. The two start out matching and then quietly drift: a field gets added or its nullability changes in the schema, and the hand-written interface doesn't get updated to match, because nothing forces it to. The mismatch usually shows up later as a runtime bug rather than a compile error, since TypeScript can only be as accurate as the types it was given in the first place.
The rule that trips up almost everyone: nullable by default
In GraphQL SDL, a bare type name like String is nullable by default, and a trailing ! is what makes it required — the opposite of how most people instinctively read it coming from TypeScript, where a bare type is required unless you explicitly add a ?. field: String means the field can be null; field: String! means it can't. Converting by hand, it's easy to skim past the ! and get the nullability backwards, especially across a schema with dozens of fields where only some carry the mark.
The convention most tooling (including GraphQL Code Generator) settles on for the TypeScript side is to make a nullable field optional and explicitly include null in its type — field?: string | null — while a non-null field stays required with no null in its type — field: string. Matching that convention, rather than inventing your own, keeps generated and hand-written types consistent if you ever mix the two.
Convert your schema
Lists compound the nullability, one level deeper
List types apply the same ! logic at two levels at once, which is where hand conversion gets genuinely error-prone. [String!]! is a required array of required strings — string[] in TypeScript, nothing nullable anywhere. [String] is a nullable array that may itself contain null entries — (string | null)[] | null. [String!]! and [String] look almost identical at a glance but produce meaningfully different TypeScript, and a schema with several list fields makes manual conversion a place where small mistakes compound quickly.
Interfaces, unions, and enums map over more directly
A GraphQL interface becomes a TypeScript interface, and a type's implements clause becomes extends — type Post implements Node becomes interface Post extends Node. A union type (union SearchResult = Author | Post) becomes a straightforward TypeScript union of the member names. Enums are the one place with a real choice to make: the modern convention is a string literal union (type Status = "DRAFT" | "PUBLISHED"), which is what most current GraphQL codegen tooling defaults to, though a traditional TypeScript enum works identically for the values themselves if that's what the rest of your codebase already uses.
When you need more than a quick conversion
A one-off schema-to-types conversion is genuinely useful for prototyping, reviewing a schema design, or building intuition for how the two type systems relate — but it isn't a substitute for a real codegen pipeline on an actively maintained API. Tools like GraphQL Code Generator introspect a live schema, regenerate types automatically whenever it changes, and can also generate fully typed query and mutation functions with field arguments included — none of which a static, one-off conversion can keep in sync over time. Reach for that kind of pipeline once a schema is actively evolving; reach for a quick conversion when you just need the shape right now.