Skip to content

Commit 2fc7ab8

Browse files
feat: add options to add __typename to interfaces
The new option `addTypenameToInterfaces` adds the `__typename` field to types resulting from interfaces. The field admits a union of string values, each the name of one of the concrete types implementing the interface.
1 parent bec7e74 commit 2fc7ab8

File tree

5 files changed

+95
-20
lines changed

5 files changed

+95
-20
lines changed

packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,9 @@ export class BaseResolversVisitor<
10711071
relevantFields: ReturnType<typeof this.getRelevantFieldsToOmit>
10721072
): string {
10731073
this._globalDeclarations.add(OMIT_TYPE);
1074-
return `Omit<${typeName}, ${relevantFields.map(f => `'${f.fieldName}'`).join(' | ')}> & { ${relevantFields
1074+
return `Omit<${typeName}, ${relevantFields
1075+
.map(f => `'${f.fieldName}'`)
1076+
.join(this.typeUnionOperator)}> & { ${relevantFields
10751077
.map(f => `${f.fieldName}${f.addOptionalSign ? '?' : ''}: ${f.replaceWithType}`)
10761078
.join(', ')} }`;
10771079
}
@@ -1222,7 +1224,7 @@ export class BaseResolversVisitor<
12221224
? 'never'
12231225
: members.length > 1
12241226
? `\n | ${members.map(m => m.replace(/\n/g, '\n ')).join('\n | ')}\n `
1225-
: members.join(' | ');
1227+
: members.join(this.typeUnionOperator);
12261228
return result;
12271229
}
12281230

@@ -1783,7 +1785,7 @@ export class BaseResolversVisitor<
17831785

17841786
protected applyRequireFields(argsType: string, fields: InputValueDefinitionNode[]): string {
17851787
this._globalDeclarations.add(REQUIRE_FIELDS_TYPE);
1786-
return `RequireFields<${argsType}, ${fields.map(f => `'${f.name.value}'`).join(' | ')}>`;
1788+
return `RequireFields<${argsType}, ${fields.map(f => `'${f.name.value}'`).join(this.typeUnionOperator)}>`;
17871789
}
17881790

17891791
protected applyOptionalFields(argsType: string, _fields: readonly InputValueDefinitionNode[]): string {
@@ -1875,7 +1877,7 @@ export class BaseResolversVisitor<
18751877
const possibleTypes = originalNode.types
18761878
.map(node => node.name.value)
18771879
.map(f => `'${f}'`)
1878-
.join(' | ');
1880+
.join(this.typeUnionOperator);
18791881

18801882
this._collectedResolvers[node.name.value] = {
18811883
typename: name + '<ContextType>',
@@ -2039,7 +2041,7 @@ export class BaseResolversVisitor<
20392041
typeName,
20402042
});
20412043

2042-
const possibleTypes = implementingTypes.map(name => `'${name}'`).join(' | ') || 'null';
2044+
const possibleTypes = implementingTypes.map(name => `'${name}'`).join(this.typeUnionOperator) || 'null';
20432045

20442046
// An Interface has __resolveType resolver, and no other fields.
20452047
const blockFields: string[] = [

packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
EnumValueDefinitionNode,
66
FieldDefinitionNode,
77
GraphQLEnumType,
8+
GraphQLInterfaceType,
89
GraphQLSchema,
910
InputObjectTypeDefinitionNode,
1011
InputValueDefinitionNode,
@@ -713,7 +714,7 @@ export class BaseTypesVisitor<
713714
const originalNode = parent[key] as UnionTypeDefinitionNode;
714715
const possibleTypes = originalNode.types
715716
.map(t => (this.scalars[t.name.value] ? this._getScalar(t.name.value, 'output') : this.convertName(t)))
716-
.join(' | ');
717+
.join(this.typeUnionOperator);
717718

718719
return new DeclarationBlock(this._declarationBlockConfig)
719720
.export()
@@ -732,22 +733,20 @@ export class BaseTypesVisitor<
732733
block.withBlock(this.mergeAllFields(fields, interfaces.length > 0));
733734
}
734735

736+
getTypenameField(type: DeclarationKind, typeNames: string[]) {
737+
const optionalTypename = this.config.nonOptionalTypename ? '__typename' : '__typename?';
738+
return `${this.config.immutableTypes ? 'readonly ' : ''}${optionalTypename}: ${typeNames
739+
.map(typeName => `'${typeName}'`)
740+
.join(this.typeUnionOperator)}${this.getPunctuation(type)}`;
741+
}
742+
735743
getObjectTypeDeclarationBlock(
736744
node: ObjectTypeDefinitionNode,
737745
originalNode: ObjectTypeDefinitionNode
738746
): DeclarationBlock {
739-
const optionalTypename = this.config.nonOptionalTypename ? '__typename' : '__typename?';
740747
const { type, interface: interfacesType } = this._parsedConfig.declarationKind;
741748
const allFields = [
742-
...(this.config.addTypename
743-
? [
744-
indent(
745-
`${this.config.immutableTypes ? 'readonly ' : ''}${optionalTypename}: '${
746-
node.name.value
747-
}'${this.getPunctuation(type)}`
748-
),
749-
]
750-
: []),
749+
...(this.config.addTypename ? [indent(this.getTypenameField(type, [node.name.value]))] : []),
751750
...node.fields,
752751
] as string[];
753752
const interfacesNames = originalNode.interfaces ? originalNode.interfaces.map(i => this.convertName(i)) : [];
@@ -790,13 +789,28 @@ export class BaseTypesVisitor<
790789
node: InterfaceTypeDefinitionNode,
791790
_originalNode: InterfaceTypeDefinitionNode
792791
): DeclarationBlock {
792+
const { type, interface: interfacesType } = this._parsedConfig.declarationKind;
793793
const declarationBlock = new DeclarationBlock(this._declarationBlockConfig)
794794
.export()
795-
.asKind(this._parsedConfig.declarationKind.interface)
795+
.asKind(interfacesType)
796796
.withName(this.convertName(node))
797797
.withComment(node.description?.value);
798798

799-
return declarationBlock.withBlock(node.fields.join('\n'));
799+
const schemaType = this._schema.getType(node.name.value);
800+
const { objects: concreteTypes } = this._schema.getImplementations(schemaType as GraphQLInterfaceType);
801+
const allFields = [
802+
...node.fields,
803+
...(this.config.addTypenameToInterfaces && concreteTypes.length > 0
804+
? [
805+
this.getTypenameField(
806+
type,
807+
concreteTypes.map(({ name }) => name)
808+
),
809+
]
810+
: []),
811+
];
812+
813+
return declarationBlock.withBlock(allFields.join('\n'));
800814
}
801815

802816
InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode, key: number | string, parent: any): string {

packages/plugins/other/visitor-plugin-common/src/base-visitor.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface ParsedConfig {
2828
typesPrefix: string;
2929
typesSuffix: string;
3030
addTypename: boolean;
31+
addTypenameToInterfaces: boolean;
3132
nonOptionalTypename: boolean;
3233
extractAllFieldsToTypes: boolean;
3334
externalFragments: LoadedFragment[];
@@ -283,6 +284,28 @@ export interface RawConfig {
283284
* ```
284285
*/
285286
skipTypename?: boolean;
287+
/**
288+
* @description Similarly to the `addTypename` option, if true, a `__typename` field will be added to type definitions resulting from interface declarations.
289+
*
290+
* @exampleMarkdown
291+
* ```ts filename="codegen.ts"
292+
* import type { CodegenConfig } from '@graphql-codegen/cli';
293+
*
294+
* const config: CodegenConfig = {
295+
* // ...
296+
* generates: {
297+
* 'path/to/file': {
298+
* // plugins...
299+
* config: {
300+
* addTypenameToInterfaces: true
301+
* },
302+
* },
303+
* },
304+
* };
305+
* export default config;
306+
* ```
307+
*/
308+
addTypenameToInterfaces?: boolean;
286309
/**
287310
* @default false
288311
* @description Automatically adds `__typename` field to the generated types, even when they are not specified
@@ -416,6 +439,7 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
416439
externalFragments: rawConfig.externalFragments || [],
417440
fragmentImports: rawConfig.fragmentImports || [],
418441
addTypename: !rawConfig.skipTypename,
442+
addTypenameToInterfaces: !rawConfig.skipTypename && rawConfig.addTypenameToInterfaces,
419443
nonOptionalTypename: !!rawConfig.nonOptionalTypename,
420444
useTypeImports: !!rawConfig.useTypeImports,
421445
allowEnumStringTypes: !!rawConfig.allowEnumStringTypes,
@@ -451,6 +475,10 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
451475
return this._parsedConfig;
452476
}
453477

478+
get typeUnionOperator() {
479+
return ' | ';
480+
}
481+
454482
public convertName(node: ASTNode | string, options?: BaseVisitorConvertOptions & ConvertOptions): string {
455483
const useTypesPrefix = typeof options?.useTypesPrefix === 'boolean' ? options.useTypesPrefix : true;
456484
const useTypesSuffix = typeof options?.useTypesSuffix === 'boolean' ? options.useTypesSuffix : true;

packages/plugins/typescript/typescript/src/visitor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class TsVisitor<
125125
}
126126

127127
if (implementingTypes.length > 0) {
128-
return implementingTypes.join(' | ');
128+
return implementingTypes.join(this.typeUnionOperator);
129129
}
130130
}
131131

@@ -255,7 +255,7 @@ export class TsVisitor<
255255
const possibleTypes = originalNode.types
256256
.map(t => (this.scalars[t.name.value] ? this._getScalar(t.name.value, 'output') : this.convertName(t)))
257257
.concat(...withFutureAddedValue)
258-
.join(' | ');
258+
.join(this.typeUnionOperator);
259259

260260
return new DeclarationBlock(this._declarationBlockConfig)
261261
.export()

packages/plugins/typescript/typescript/tests/typescript.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3950,6 +3950,37 @@ describe('TypeScript', () => {
39503950
`);
39513951
});
39523952

3953+
it('should union concrete type names in interface __typename field - issue #10522', async () => {
3954+
debugger;
3955+
const testSchema = buildSchema(/* GraphQL */ `
3956+
interface TopLevel {
3957+
topLevelField: Boolean
3958+
}
3959+
3960+
type OneImplementation implements TopLevel {
3961+
topLevelField: Boolean
3962+
implementationField: String
3963+
}
3964+
3965+
type AnotherImplementation implements TopLevel {
3966+
topLevelField: Boolean
3967+
anotherImplementationField: Int
3968+
}
3969+
`);
3970+
const output = await plugin(
3971+
testSchema,
3972+
[],
3973+
{
3974+
addTypenameToInterfaces: true,
3975+
},
3976+
{ outputFile: 'graphql.ts' }
3977+
);
3978+
3979+
expect(output.content).toBeSimilarStringTo(`
3980+
__typename?: 'OneImplementation' | 'AnotherImplementation'
3981+
`);
3982+
});
3983+
39533984
it('should use implementing types as node type - issue #5126', async () => {
39543985
const testSchema = buildSchema(/* GraphQL */ `
39553986
type Matrix {

0 commit comments

Comments
 (0)