Skip to content

Type Safety ​

Type safety catches errors before your code runs, turning runtime bugs into compile-time errors.

The Problem with Runtime Errors ​

Without type safety, mistakes only surface when code executes:

typescript
const { data } = await fetchUser(userId);

console.log(data.user.nmae);

This code compiles successfully but crashes at runtime. The typo nmae instead of name goes unnoticed until a user triggers this code path.

With GraphQL, the problem compounds. Your schema defines what fields exist, but without type generation, nothing ensures your queries match:

typescript
const query = `
  query {
    user(id: "123") {
      name
      emial
    }
  }
`;

The GraphQL server rejects this query because emial doesn't exist. But you don't discover this until the request fails in production.

Compile-Time Validation ​

Type safety moves these errors to compile time. Your editor shows errors immediately:

typescript
const { data } = useQuery(
  graphql(`
    query GetUserQuery($id: ID!) {
      user(id: $id) {
        name
        email
      }
    }
  `),
  { id: userId },
);

console.log(data.user.nmae);

TypeScript underlines nmae in red. The error appears in your editor before you save, preventing the bug entirely.

Full-Stack Type Flow ​

Type safety works across your entire stack:

Schema → Operations ​

Your GraphQL schema defines available types and fields. Build tools parse the schema and validate your operations against it. Queries requesting non-existent fields fail at build time:

typescript
graphql(`
  query {
    user(id: "123") {
      invalidField
    }
  }
`);

Error: Field "invalidField" does not exist on type "User"

Operations → Variables ​

Type generation creates interfaces for variables. Pass the wrong type and TypeScript catches it:

typescript
const GetUserQuery = graphql(`
  query GetUserQuery($id: ID!) {
    user(id: $id) {
      name
    }
  }
`);

useQuery(GetUserQuery, { id: 123 });

Error: Type 'number' is not assignable to type 'string'

Variables → Components ​

Generated types flow through to your components. Access non-existent properties and the compiler stops you:

typescript
const { data } = useQuery(
  graphql(`
    query GetUserQuery($id: ID!) {
      user(id: $id) {
        name
        email
      }
    }
  `),
  { id: userId },
);

return <div>{data.user.phoneNumber}</div>;

Error: Property 'phoneNumber' does not exist on type 'User'

Developer Experience Benefits ​

Type safety provides immediate feedback while coding:

Autocomplete ​

Your editor suggests available fields as you type. No need to reference documentation or remember field names:

typescript
data.user.

Autocomplete shows: id, name, email, avatar, bio, createdAt

Refactoring ​

Rename a field in your schema and TypeScript shows every usage that needs updating. No grep, no missed references:

  1. Rename email to emailAddress in schema
  2. Build fails, showing all locations using email
  3. Update each location with confidence
  4. Build succeeds, nothing broken

Documentation ​

Hover over any field to see its type and documentation from your schema:

typescript
data.user.createdAt;

Tooltip shows: createdAt: DateTime - The date this user was created

Null Safety ​

GraphQL's null semantics transfer to TypeScript. Non-nullable fields in your schema become non-optional properties:

graphql
type User {
  id: ID!
  name: String!
  bio: String
}

Generated TypeScript:

typescript
interface User {
  id: string;
  name: string;
  bio: string | null;
}

Access bio without checking and TypeScript warns you:

typescript
const length = data.user.bio.length;

Error: Object is possibly 'null'

How Type Generation Works ​

Type generation happens automatically at build time:

  1. Schema Loading - Build tool reads your GraphQL schema
  2. Operation Extraction - Parser finds all graphql() calls in your code
  3. Validation - Each operation is validated against the schema
  4. Type Generation - TypeScript types are generated for each operation
  5. File Output - Types are written to a generated file

This happens during development as you save files. No separate generation step required.

Zero Runtime Overhead ​

Type generation produces only compile-time types. Generated code contains no type information:

typescript
const GetUserQuery = graphql(`
  query GetUserQuery($id: ID!) {
    user(id: $id) {
      name
    }
  }
`);

Compiles to:

javascript
const GetUserQuery = {
  query: 'query GetUserQuery($id: ID!) { user(id: $id) { name } }',
  operationName: 'GetUserQuery',
};

No types in the bundle. All type safety at compile time, zero cost at runtime.

Beyond Basic Types ​

Advanced type features handle complex scenarios:

Conditional Fields ​

Fields selected conditionally are typed as optional:

typescript
graphql(`
  query GetUserQuery($id: ID!, $includeBio: Boolean!) {
    user(id: $id) {
      name
      bio @include(if: $includeBio)
    }
  }
`);

Generated type:

typescript
interface GetUserQueryData {
  user: {
    name: string;
    bio?: string | null;
  };
}

Unions and Interfaces ​

GraphQL unions and interfaces map to TypeScript discriminated unions:

graphql
interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String!
}

type Post implements Node {
  id: ID!
  title: String!
}

Generated types use __typename for discrimination:

typescript
type Node = { __typename: 'User'; id: string; name: string } | { __typename: 'Post'; id: string; title: string };

TypeScript narrows types based on __typename:

typescript
if (node.__typename === 'User') {
  console.log(node.name);
}

Fragment Types ​

Fragments generate reusable type fragments:

typescript
const UserCard_user = graphql(`
  fragment UserCard_user on User {
    name
    avatar
  }
`);

Component props use generated fragment reference types:

typescript
interface UserCardProps {
  user: UserCard_user$key;
}

This ensures the fragment is spread correctly in parent queries.

Type Safety vs Runtime Safety ​

Type safety catches errors at compile time but doesn't validate runtime data. If your server returns unexpected data, TypeScript can't help:

typescript
const data = { user: { name: 123 } };

This satisfies TypeScript if typed as { user: { name: string } } but crashes at runtime when you try name.toUpperCase().

GraphQL provides runtime schema validation server-side. Your server rejects malformed responses before they reach the client. This combines with compile-time types for comprehensive safety.

Next Steps ​