I’m working on a legacy codebase where, in our GraphQL schema, an interface was defined as ‘Subscription’:
interface Subscription {
fieldOne: String
fieldTwo: String
}
type FirstSubscriptionType implements Subscription {
anotherField: String
}
type SecondSubscriptionType implements Subscription {
yetAnotherField: String
}
This has worked fine (we don’t use subscriptions and don’t have plans to), but when I try to use graphql-codegen
to auto-generate Typescript types for us to use in our frontend code, the generates
step fails with this error:
Subscription root type must be Object type if provided, it cannot be Subscription
The obvious answer is to rename the interface. Sure enough, renaming to something like ISubscription
fixes everything. I’m wondering if there’s a way to avoid having to change anything in our schema and have graphql-codegen
translate/rename the type in just the frontend code. Is this possible?
1 Answer
Bit late to the party but I have found an answer.
I leveraged the lifecycle hooks that GraphQL Codegen exposes to call a typescript transformer script which de-dupes the type definitions.
Configure your codegen as shown below, so it calls the script after generating the file(s). If you need to de-dupe multiple files, you can change the lifecycle hook from afterOneFileWrite
to afterAllFileWrite
. The script will receive the filepath for the generated file(s) as process args.
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
overwrite: true,
schema: [
{
"/source/": {},
},
],
hooks: {
afterOneFileWrite: ["ts-node scripts/resolve-type-conflicts"],
afterAllFileWrite: ["prettier --write"],
},
documents: [
/** etc */
],
generates: {
"src/": {
/* Whatever your config is */
},
},
};
export default config;
Then write the resolve-type-conflicts.ts
script as such
import * as fs from "fs";
import * as ts from "typescript";
const resolveTypeConflicts = () => {
// this will be the generated file from codegen
const [, , file] = process.argv;
// we track how often we have seen a given variable name here
const collisionsCounter: Record<string, number> = {};
const transformerFactory: ts.TransformerFactory<ts.Node> =
(context) => (rootNode) => {
function visitTypeAliasDeclaration(node: ts.Node): ts.Node {
// recurse through the Typescript AST of the file
node = ts.visitEachChild(
node,
visitTypeAliasDeclaration,
context
);
/**
*
* short-circuit logic to prevent handling nodes we don't
* care about. This might need to be adjusted based on what
* you need to de-dupe. In my case only types were being duped
*/
if (!ts.isTypeAliasDeclaration(node)) {
return node;
}
/**
* you may not need to cast to lowercase. In my case I had
* name collisions that different cases which screwed up a
* later step
*/
const nameInLowerCase = node.name.text.toLowerCase();
const suffix = collisionsCounter[nameInLowerCase] ?? "";
const encounterCount = collisionsCounter[nameInLowerCase];
collisionsCounter[nameInLowerCase] =
typeof encounterCount === "undefined"
? 1
: encounterCount + 1;
return context.factory.createTypeAliasDeclaration(
node.modifiers,
`${node.name.text}${suffix}`,
node.typeParameters,
node.type
);
}
return ts.visitNode(rootNode, visitTypeAliasDeclaration);
};
const program = ts.createProgram([file], {});
const sourceFile = program.getSourceFile(file);
const transformationResult = ts.transform(sourceFile, [transformerFactory]);
const transformedSourceFile = transformationResult.transformed[0];
const printer = ts.createPrinter();
// the deduped code is here as a string
const result = printer.printNode(
ts.EmitHint.Unspecified,
transformedSourceFile,
undefined
);
fs.writeFileSync(file, result, {});
};
resolveTypeConflicts();