graphql-code-generator icon indicating copy to clipboard operation
graphql-code-generator copied to clipboard

Support type-only TypedDocumentNode declarations

Open chriskrycho opened this issue 5 years ago • 12 comments

Currently, given a query like this—

query findUser($userId: ID!) {
    user(id: $userId) {
        id
        username
        role
    }
}

—the generator creates this output (assuming typescript-operations and near-operation-file as well for convenience):

import * as Types from './types';

import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type FindUserQueryVariables = Types.Exact<{
  userId: Types.Scalars['ID'];
}>;


export type FindUserQuery = { __typename?: 'Query', user?: Types.Maybe<{ __typename?: 'User', id: string, username: string, role: Types.Role }> };


export const FindUserDocument: DocumentNode<FindUserQuery, FindUserQueryVariables> = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"findUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},"directives":[]}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"username"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"role"},"arguments":[],"directives":[]}]}}]}}]};

For many purposes, however, there's zero value in the actual TS output, only a type definition, and we in fact want to generate only a .d.ts file, which should look something like this:

import * as Types from './types';

import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type FindUserQueryVariables = Types.Exact<{
  userId: Types.Scalars['ID'];
}>;


export type FindUserQuery = { __typename?: 'Query', user?: Types.Maybe<{ __typename?: 'User', id: string, username: string, role: Types.Role }> };


export const FindUserDocument: DocumentNode<FindUserQuery, FindUserQueryVariables>;

chriskrycho avatar Sep 02 '20 14:09 chriskrycho

Note: I'm presently working around this with the slightly ridiculous hack of using a regex to remove everything after the DocumentNode<...> declaration. This is obviously fragile and not a good long-term solution for us!

chriskrycho avatar Sep 02 '20 14:09 chriskrycho

The problem here appears to be upstream: ClientSideBaseVisitor#OperationDefinition bails if the node is not named, so we never even hit the point where the TypeScriptDocumentNodesVisitor's implementation can run. Seems reasonable to move this issue upstream?

I'll open a fix this, and I'm poking through the implementation of ClientSideBaseVisitor to get a handle on how now—but it's not actually clear to me what constraints need to hold with ClientSideBaseVisitor#OperationDefinition.

chriskrycho avatar Sep 02 '20 15:09 chriskrycho

Will be fixed by dotansimha/graphql-code-generator#4682.

chriskrycho avatar Sep 02 '20 16:09 chriskrycho

@dotansimha I'm not sure why, but I filed those last two comments as though they were related to this issue… and they're not. 😆 I think we just need an option for this plugin to solve this!

chriskrycho avatar Sep 14 '20 16:09 chriskrycho

Wanted to second this! I'm using an approach along these lines, letting graphql-tag/loader or similar handle the actual concrete JS generation.

Using this plugin: https://github.com/dleavitt/apollo-typed-documents

And a config like this:

overwrite: true
schema: "./src/graphql/schema.graphql"
documents: "./src/**/*.{gql,graphql}"
generates:
  src/schema.ts:
    plugins:
      - "typescript"
      - "typescript-operations"

  src/operations.d.ts:
    plugins:
      # - "typescript-graphql-files-modules"
      - "@dleavitt/apollo-typed-documents/lib/codegenTypedDocuments"
    config:
      relativeToCwd: true
      prefix: ""
      typesModule: "schema"

You get the following. Quite a bit tidier.

# src/operations.d.ts
declare module "apps/account/AccountOperations.gql" {
  import { TypedDocumentNode } from "@apollo/client"
  import {
    UserGetCurrentQuery,
    UserGetCurrentQueryVariables,
  } from "schema"
  export const UserGetCurrent: TypedDocumentNode<
    UserGetCurrentQueryVariables,
    UserGetCurrentQuery
  >
  import {
    UserAccountGetQuery,
    UserAccountGetQueryVariables,
  } from "schema"
  export const UserAccountGet: TypedDocumentNode<
    UserAccountGetQueryVariables,
    UserAccountGetQuery
  >
  import {
    UserAccountUpdateMutation,
    UserAccountUpdateMutationVariables,
  } from "schema"
  export const UserAccountUpdate: TypedDocumentNode<
    UserAccountUpdateMutationVariables,
    UserAccountUpdateMutation
  >
}
// NB that the type param ordering for TypedDocumentNode is backwards here

dleavitt avatar Sep 29 '20 04:09 dleavitt

Yeah this could work :) Requires some changes in the codegen plugin, but it could be done.

Btw, can you please share how do you use .d.ts files and those declarations? As far as I know, you can't declare module augmentation for a local file (declare module "./my-file.graphql" wont work since it must be a wildcard or a package name).

dotansimha avatar Sep 29 '20 08:09 dotansimha

All you have to do in this case is generate my-file.graphql.js and my-file.graphql.d.ts—using some other mechanism for the JS file and this project to codegen the .d.ts file.

chriskrycho avatar Sep 29 '20 13:09 chriskrycho

@dotansimha I think it just works? I've got my .d.ts files specified explicitly in my tsconfig but nothing special beyond that.

Requires some changes in the codegen plugin, but it could be done.

Could also do this approach as a separate plugin. I'm happy to clean up the fork I'm working from if you think people would find it useful.

dleavitt avatar Sep 30 '20 01:09 dleavitt

@chriskrycho I'm moving this to codegen repo, let's track it there :)

dotansimha avatar Nov 05 '20 07:11 dotansimha

Just chiming in to express my interest in this feature. It would enable me to add TypedDocumentNode support to apollo elements

// import { HelloQueryData, HelloQueryVariables } from '#schema'; // old style codegen
import { HelloQueryDocument } from '#schema'; // after this issue is merged

import HelloQuery from './Hello.query.graphql'; // actual `.graphql` file, transformed with a build step or dev-server plugin

// old style codegen, but still supported with some type inference
class OldStyleHelloQueryElement extends ApolloQuery<HelloQueryData, HelloQueryVariables> {
  query = HelloQuery;
}

// new style, accepting a TypedDocumentNode
class HelloQueryElement extends ApolloQuery<HelloQueryDocument> {
  query = HelloQuery;
}

OR

import { HelloQueryDocument } from '#schema'; // full codegen
// OR
import { HelloQueryDocument } from './Hello.query'; // file codegen

// also supported via `typeof`
class HelloQueryElement extends ApolloQuery<typeof HelloQueryDocument> {
  query = HelloQueryDocument;
}

bennypowers avatar Nov 24 '20 11:11 bennypowers

TL:DR: Is it worth enhancing the addOperationExport feature instead?

Hello from five years in the future! I also need a declaration only version of the typed-document-node plugin. Back in 2020, a somewhat related feature was added to the typescript-operations plugin:

  • #4020

Given this fabricated query:

# getUser.gql
query getUser {
    currentUser {id}
}

A declaration only, near-operation-file setup with addOperationExport might generate something like this. The export naming lines up with how graphql-tag/laoder works in webpack:

// getUser.d.gql.ts
import * as Types from "../schema.generated.js";

export type GetUserQueryVariables = Types.Exact<{[key: string]: never}>;

export type GetUserQuery = {
    __typename?: "Query";
    currentUser?: {
        __typename?: "User";
        id: string;
    } | null;
};

// This is the most important line for this discussion 👇 
export declare const getUser: import("graphql").DocumentNode;

This gets us pretty close, but you can tell straight away it's not very strongly typed. What we really want is an option to do this:

- export declare const getUser: import("graphql").DocumentNode;
+ export declare const getUser: import("@graphql-typed-document-node/core").TypedDocumentNode<GetUserQuery, GetUserQueryVariables>;

And as an added bonus, it should also support the first-document-is-the-default-export behavior of graphql-tag/loader:

export default getUser;

I have a strawman running locally that does the above, but it feels very hacky and not the graphql-codgen-way™. I would probably need some help to tighten it up, assuming there is interest in pursuing addOperationExport as an alternative?

benquarmby avatar Nov 17 '25 23:11 benquarmby

Here is a strawman plugin. It's very basic without support for naming conventions, anonymous queries etc. But it shows the concept:

// graphql-typed-exports-plugin.mjs
import {pascalCase} from "change-case-all";

export function plugin(schema, documents) {
    let firstExport;

    const outputs = documents.map(function ({document}, index) {
        const [rootDefinition] = document.definitions;
        const documentName = rootDefinition.name.value;
        const outputType = pascalCase(documentName) + pascalCase(rootDefinition.operation);
        const inputType = outputType + "Variables";

        if (index === 0) {
            firstExport = documentName;
        }

        return `export declare const ${documentName}: import("@graphql-typed-document-node/core").TypedDocumentNode<${outputType}, ${inputType}>;`;
    });

    if (firstExport) {
        outputs.push(`export default ${firstExport};`);
    }

    return outputs.join("\n");
}

benquarmby avatar Nov 18 '25 17:11 benquarmby