Support type-only TypedDocumentNode declarations
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>;
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!
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.
Will be fixed by dotansimha/graphql-code-generator#4682.
@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!
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
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).
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.
@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.
@chriskrycho I'm moving this to codegen repo, let's track it there :)
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;
}
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?
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");
}