Codegen

Typed Queries

Generate TypedDocumentNode exports for fully typed GraphQL queries

Overview

The @universal-data-layer/codegen-typed-queries extension generates TypedDocumentNode exports from your .graphql files. This provides complete type safety for both query results and variables.

Installation

Terminal
npm install @universal-data-layer/codegen-typed-queries

Configuration

Add the extension to your codegen config:

udl.config.ts
import { defineConfig } from 'universal-data-layer';

export const config = defineConfig({
  plugins: [
    {
      name: '@universal-data-layer/plugin-source-contentful',
      options: {
        spaceId: process.env['CONTENTFUL_SPACE_ID'],
        accessToken: process.env['CONTENTFUL_ACCESS_TOKEN'],
      },
    },
  ],
  codegen: {
    output: './generated',
    extensions: ['@universal-data-layer/codegen-typed-queries'],
  },
});

Creating Query Files

Create .graphql files anywhere in your project:

app/queries/products.graphql
query GetAllProducts {
  allContentfulProducts {
    name
    slug
    price
    description
  }
}

query GetProductBySlug($slug: String!) {
  contentfulProduct(slug: $slug) {
    name
    slug
    price
    description
    image {
      ... on ContentfulAsset {
        file {
          url
        }
      }
    }
  }
}

Generated Output

After running the server (which triggers codegen), you'll have:

generated/
├── queries/
│   └── index.ts     # TypedDocumentNode exports
├── types/
│   └── index.ts     # Schema types
└── index.ts         # Re-exports everything

The queries file contains:

// generated/queries/index.ts

export interface GetAllProductsResult {
  allContentfulProducts: Array<{
    name: string;
    slug: string;
    price: number;
    description: string;
  }>;
}

export const GetAllProducts: TypedDocumentNode<
  GetAllProductsResult,
  Record<string, never>
>;

export interface GetProductBySlugVariables {
  slug: string;
}

export interface GetProductBySlugResult {
  contentfulProduct: {
    name: string;
    slug: string;
    price: number;
    description: string;
    image: {
      file: {
        url: string;
      };
    };
  } | null;
}

export const GetProductBySlug: TypedDocumentNode<
  GetProductBySlugResult,
  GetProductBySlugVariables
>;

Using Generated Queries

Import and use the generated queries with the UDL client:

import { udl } from 'universal-data-layer/client';
import { GetAllProducts, GetProductBySlug } from '@/generated/queries';

// Result type is automatically inferred
const [error, products] = await udl.query(GetAllProducts);

if (error) {
  console.error('Failed to fetch products:', error.message);
  return;
}

// TypeScript knows products is Array<{ name, slug, price, description }>
for (const product of products) {
  console.log(product.name, product.price);
}

With variables:

import { udl } from 'universal-data-layer/client';
import { GetProductBySlug } from '@/generated/queries';

// Variables are type-checked - must include { slug: string }
const [error, product] = await udl.query(GetProductBySlug, {
  variables: { slug: 'my-product' },
});

if (error || !product) {
  return;
}

// TypeScript knows the exact shape of product
console.log(product.name, product.image?.file.url);

Type Inference

The extension generates precise types based on your query's selection set:

  • Selection sets - Only fields you request are in the result type
  • Variables - Required/optional based on ! in the query definition
  • Nullable fields - Marked as | null when appropriate
  • Arrays - Detected from schema and typed correctly

Query Discovery

The extension automatically discovers .graphql and .gql files in your project. It searches:

  • All directories except node_modules, .git, and your output directory
  • Both .graphql and .gql extensions
  • Multiple queries per file are supported

Limitations

  • Fragments - Not yet fully supported. Inline fragment types work, but shared fragments may not resolve correctly.
  • Custom scalars - Default to unknown type. Define custom scalar mappings if needed.
  • Named operations only - Anonymous queries are skipped. Always name your operations.

Alternative: graphql-codegen

For advanced features like fragment support, consider using GraphQL Code Generator:

Terminal
npm install @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
codegen.yml
schema: http://localhost:4000/graphql
documents: 'app/**/*.graphql'
generates:
  ./generated/operations.ts:
    plugins:
      - typescript
      - typescript-operations
      - typed-document-node

Next Steps