Skip to content

Fix: gql.tada Not Working — Types Not Inferred, Schema Not Found, or IDE Not Showing Completions

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix gql.tada issues — schema introspection, type-safe GraphQL queries, fragment masking, urql and Apollo Client integration, IDE setup, and CI type checking.

The Problem

graphql() returns unknown instead of typed data:

import { graphql } from 'gql.tada';

const UserQuery = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`);
// TypeScript shows the result type as unknown — no inference

Or the IDE shows no autocompletion inside the template literal:

const query = graphql(`
  query {
    |  // No suggestions after typing
  }
`);

Or the schema file can’t be found:

Error: Could not find schema — or —
gql.tada: No schema found in tsconfig.json

Why This Happens

gql.tada infers GraphQL types at the TypeScript level — no codegen step needed. But it requires specific configuration:

  • The GraphQL schema must be available to TypeScript — gql.tada reads a schema file (introspection JSON or SDL) referenced in tsconfig.json. Without this, it can’t infer field types from queries.
  • The TypeScript plugin must be configured — gql.tada uses a TypeScript language service plugin for IDE autocompletion. Without the plugin in tsconfig.json, the IDE doesn’t know about the GraphQL schema.
  • Schema must be up to date — gql.tada reads the schema at build/typecheck time. If the API changed but the local schema file is outdated, types don’t match reality.
  • Template literal types have limits — TypeScript’s template literal inference works with graphql() from gql.tada’s specific import. Using a different graphql tag or wrapping it in another function breaks inference.

The deeper reason people get stuck is that gql.tada has two distinct moving parts, and they fail independently. Part one is the TypeScript type inference, which runs purely in the type system from the introspection type defined in graphql-env.d.ts. If graphql-env.d.ts is missing or stale, every query returns unknown at compile time even though the editor pretends nothing is wrong. Part two is the language service plugin @0no-co/graphqlsp, which runs inside the TypeScript server and provides field autocompletion, hover docs, and red squiggles for invalid queries. If the LSP is installed but the editor is using its bundled TypeScript instead of the workspace one, the plugin never loads — the types still infer correctly because that runs in the standalone compiler, but you get zero IDE help.

The second class of failure is editor-specific. VS Code defaults to its bundled TypeScript, which silently ignores the plugins array in tsconfig.json. You have to switch to the workspace version explicitly. WebStorm uses the project TypeScript by default but has its own quirk: the GraphQL plugin must be disabled, otherwise it competes with graphqlsp for the same regions of the template literal and you get duplicated or wrong completions. Cursor and the Zed editor both use the workspace TypeScript by default but require the same “use workspace version” reminder for new contributors.

The third class is the codegen alternative. Apollo, urql, and Relay each have their own codegen pipelines that pre-generate types from operations. gql.tada coexists with them by using only the TypeScript type system — there is no codegen step to fall out of sync. If you are migrating from @graphql-codegen and still have a generated gql/ directory, both systems will type-check successfully but you will be carrying double the dependency weight. Pick one.

Fix 1: Setup and Schema Introspection

npm install gql.tada
npm install -D @0no-co/graphqlsp  # TypeScript LSP plugin for IDE support
# Generate introspection schema from your GraphQL API
npx gql.tada generate-schema "http://localhost:4000/graphql" --output schema.graphql

# Or from a file
npx gql.tada generate-schema ./schema.graphql --output introspection.ts

# Generate the output file used by TypeScript
npx gql.tada generate-output --tsconfig ./tsconfig.json
// tsconfig.json — configure the TypeScript plugin
{
  "compilerOptions": {
    "strict": true,
    "plugins": [
      {
        "name": "@0no-co/graphqlsp",
        "schema": "./schema.graphql",
        "tadaOutputLocation": "./src/graphql-env.d.ts"
      }
    ]
  }
}
// src/graphql/tada.ts — initialize gql.tada
import { initGraphQLTada } from 'gql.tada';
import type { introspection } from '../graphql-env';

export const graphql = initGraphQLTada<{
  introspection: introspection;
  scalars: {
    DateTime: string;
    JSON: Record<string, unknown>;
  };
}>();

export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
export { readFragment } from 'gql.tada';

Fix 2: Type-Safe Queries

import { graphql, type ResultOf, type VariablesOf } from '@/graphql/tada';

// Query — types are inferred from the schema
const GetUsersQuery = graphql(`
  query GetUsers($limit: Int, $offset: Int) {
    users(limit: $limit, offset: $offset) {
      id
      name
      email
      role
      createdAt
      posts {
        id
        title
      }
    }
  }
`);

// ResultOf<typeof GetUsersQuery> = {
//   users: {
//     id: string;
//     name: string;
//     email: string;
//     role: string;
//     createdAt: string;
//     posts: { id: string; title: string }[];
//   }[];
// }

// VariablesOf<typeof GetUsersQuery> = {
//   limit?: number | null;
//   offset?: number | null;
// }

// Mutation
const CreateUserMutation = graphql(`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`);

// Variables are type-checked
type CreateUserVars = VariablesOf<typeof CreateUserMutation>;
// { input: { name: string; email: string; role?: string } }

Fix 3: Fragments

import { graphql, readFragment, type FragmentOf } from '@/graphql/tada';

// Define a fragment
const UserFragment = graphql(`
  fragment UserFields on User {
    id
    name
    email
    avatar
  }
`);

// Use fragment in a query
const GetUserQuery = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      ...UserFields
      posts {
        id
        title
      }
    }
  }
`, [UserFragment]);  // Pass fragments as second argument

// Component that accepts fragment data
function UserCard({ user }: { user: FragmentOf<typeof UserFragment> }) {
  // readFragment unmasks the fragment data
  const data = readFragment(UserFragment, user);
  // data = { id: string, name: string, email: string, avatar: string }

  return (
    <div>
      <img src={data.avatar} alt={data.name} />
      <h3>{data.name}</h3>
      <p>{data.email}</p>
    </div>
  );
}

// Nested fragments
const PostFragment = graphql(`
  fragment PostFields on Post {
    id
    title
    body
    author {
      ...UserFields
    }
  }
`, [UserFragment]);

Fix 4: urql Integration

npm install urql gql.tada
'use client';

import { useQuery, useMutation } from 'urql';
import { graphql, type ResultOf } from '@/graphql/tada';

const PostsQuery = graphql(`
  query Posts($limit: Int!) {
    posts(limit: $limit) {
      id
      title
      excerpt
      author {
        name
      }
    }
  }
`);

function PostList() {
  const [result] = useQuery({
    query: PostsQuery,
    variables: { limit: 10 },
    // Variables are type-checked automatically
  });

  if (result.fetching) return <div>Loading...</div>;
  if (result.error) return <div>Error: {result.error.message}</div>;

  // result.data is fully typed
  return (
    <ul>
      {result.data?.posts.map(post => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.excerpt}</p>
          <span>by {post.author.name}</span>
        </li>
      ))}
    </ul>
  );
}

// Mutation
const CreatePostMutation = graphql(`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
    }
  }
`);

function CreatePostForm() {
  const [, createPost] = useMutation(CreatePostMutation);

  async function handleSubmit(data: { title: string; body: string }) {
    const result = await createPost({
      input: data,  // Type-checked against CreatePostInput
    });

    if (result.data) {
      console.log('Created:', result.data.createPost.id);
    }
  }

  return <form onSubmit={/* ... */}>...</form>;
}

Fix 5: Apollo Client Integration

import { useQuery, useMutation } from '@apollo/client';
import { graphql, type ResultOf, type VariablesOf } from '@/graphql/tada';

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

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useQuery(GetUserQuery, {
    variables: { id: userId },
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  // data is typed as ResultOf<typeof GetUserQuery>
  return (
    <div>
      <h1>{data?.user?.name}</h1>
      <p>{data?.user?.email}</p>
    </div>
  );
}

Fix 6: CI/CD Schema Validation

// package.json
{
  "scripts": {
    "schema:update": "gql.tada generate-schema http://localhost:4000/graphql --output schema.graphql",
    "graphql:generate": "gql.tada generate-output --tsconfig ./tsconfig.json",
    "graphql:check": "gql.tada check --tsconfig ./tsconfig.json",
    "predev": "npm run graphql:generate",
    "prebuild": "npm run graphql:generate"
  }
}
# .github/workflows/graphql-check.yml
name: GraphQL Type Check
on: [pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx gql.tada generate-output
      - run: npx tsc --noEmit  # Type-check all queries against schema

Fix 7: Platform Differences — Editor, Codegen, and Monorepo Schema Sharing

VS Code vs WebStorm vs Cursor: gql.tada’s type inference works in any TypeScript-aware tool, but autocompletion needs @0no-co/graphqlsp running inside the TypeScript Language Service. Each editor needs slightly different handling:

  • VS Code: open the Command Palette, run TypeScript: Select TypeScript Version, choose Use Workspace Version. Without this, VS Code uses its bundled TS, which ignores the plugins array. Reload the window after switching.
  • WebStorm / IntelliJ: enable Use TypeScript from node_modules in Settings -> Languages -> TypeScript. Also disable the GraphQL plugin’s “Inject GraphQL into template literals” option, because it duplicates highlighting and breaks graphqlsp’s offsets.
  • Cursor: same as VS Code — TypeScript: Select TypeScript Version -> workspace.
  • Zed: workspace TS by default; no extra action required, but the GraphQL extension should be set to passive mode.
  • Neovim with typescript-language-server: pass --tsserver-path=node_modules/typescript/lib in the LSP config so the plugin loads.

Apollo vs urql vs Relay codegen: gql.tada replaces codegen, so the comparison is about whether you keep a codegen step alongside gql.tada or not:

Clientgql.tada works with itPre-existing codegenRecommended setup
Apollo ClientYes@apollo/client/codegenDrop codegen; use graphql() from your tada.ts and pass it to useQuery
urqlYes (built-in support)@graphql-codegen/typescript-urqlDrop codegen; urql infers from the document object
RelayPartialrelay-compiler (required for .graphql files)Keep Relay codegen — Relay’s runtime needs the compiled artifacts

Relay is the exception: its runtime needs the artifacts from relay-compiler, so you cannot drop codegen entirely. With Apollo and urql you can.

Monorepo schema sharing: in a monorepo with multiple apps that share a GraphQL API, store the introspection result in a shared package:

# packages/graphql-schema/schema.graphql — committed
# packages/graphql-schema/src/index.ts — exports the introspection type

Then in each app’s tsconfig.json:

{
  "compilerOptions": {
    "plugins": [{
      "name": "@0no-co/graphqlsp",
      "schema": "../../packages/graphql-schema/schema.graphql",
      "tadaOutputLocation": "./src/graphql-env.d.ts"
    }]
  }
}

Each app keeps its own graphql-env.d.ts (it is generated, do not commit) but reads from the shared schema file. Update the schema in one place and all apps see the change after gql.tada generate-output runs.

Schema reload triggers: the LSP reads schema.graphql once at editor startup and watches the file for changes. If you regenerate the schema while the editor is open, completions update within a few seconds. But the type side — graphql-env.d.ts — only refreshes when you re-run gql.tada generate-output. Add it to a predev script so npm run dev always picks up the latest schema:

{
  "scripts": {
    "predev": "gql.tada generate-output",
    "dev": "next dev"
  }
}

For CI, gate PRs on npx gql.tada turbo or npx gql.tada check so a schema change that breaks an operation fails the build instead of merging silently.

Still Not Working?

Types are unknown — no inference — the schema isn’t configured. Check tsconfig.json has the @0no-co/graphqlsp plugin with the correct schema path. Run npx gql.tada generate-output to create the graphql-env.d.ts file. Ensure you import graphql from your initialized tada.ts, not from gql.tada directly.

No autocompletion in the IDE — VS Code must use the workspace TypeScript version. Open Command Palette -> “TypeScript: Select TypeScript Version” -> “Use Workspace Version”. The @0no-co/graphqlsp plugin only runs with the workspace TS.

Schema is outdated — run npx gql.tada generate-schema <url> to re-download the schema. The introspection result reflects the API at download time. Add schema:update to your CI or pre-dev scripts.

Fragment types don’t propagate — fragments must be passed as the second argument to graphql(): graphql(\query { …UserFields }`, [UserFragment])`. Without this, gql.tada can’t resolve fragment spreads and the fields are missing from the result type.

Completions work in VS Code but not WebStorm — the WebStorm GraphQL plugin is injecting into the template literal and competing with graphqlsp. Disable it under Settings -> Languages -> GraphQL or restrict it to .graphql files only.

Types break after upgrading TypeScript@0no-co/graphqlsp and gql.tada are versioned against specific TypeScript ranges. Check the changelog for both packages; pin TypeScript to a version both support, then upgrade them together.

Monorepo app sees unknown but a sibling app types correctly — the failing app’s tadaOutputLocation points to a file that does not exist yet. Run gql.tada generate-output --tsconfig ./apps/failing-app/tsconfig.json and verify a graphql-env.d.ts was created inside that app.

For related GraphQL issues, see Fix: GraphQL Yoga Not Working, Fix: Pothos Not Working, Fix: SWR Not Working, and Fix: Turborepo Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles