diff --git a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index df1f1105110..4ef0bcdd100 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts @@ -1071,7 +1071,9 @@ export class BaseResolversVisitor< relevantFields: ReturnType ): string { this._globalDeclarations.add(OMIT_TYPE); - return `Omit<${typeName}, ${relevantFields.map(f => `'${f.fieldName}'`).join(' | ')}> & { ${relevantFields + return `Omit<${typeName}, ${relevantFields + .map(f => `'${f.fieldName}'`) + .join(this.typeUnionOperator)}> & { ${relevantFields .map(f => `${f.fieldName}${f.addOptionalSign ? '?' : ''}: ${f.replaceWithType}`) .join(', ')} }`; } @@ -1222,7 +1224,7 @@ export class BaseResolversVisitor< ? 'never' : members.length > 1 ? `\n | ${members.map(m => m.replace(/\n/g, '\n ')).join('\n | ')}\n ` - : members.join(' | '); + : members.join(this.typeUnionOperator); return result; } @@ -1783,7 +1785,7 @@ export class BaseResolversVisitor< protected applyRequireFields(argsType: string, fields: InputValueDefinitionNode[]): string { this._globalDeclarations.add(REQUIRE_FIELDS_TYPE); - return `RequireFields<${argsType}, ${fields.map(f => `'${f.name.value}'`).join(' | ')}>`; + return `RequireFields<${argsType}, ${fields.map(f => `'${f.name.value}'`).join(this.typeUnionOperator)}>`; } protected applyOptionalFields(argsType: string, _fields: readonly InputValueDefinitionNode[]): string { @@ -1875,7 +1877,7 @@ export class BaseResolversVisitor< const possibleTypes = originalNode.types .map(node => node.name.value) .map(f => `'${f}'`) - .join(' | '); + .join(this.typeUnionOperator); this._collectedResolvers[node.name.value] = { typename: name + '', @@ -2039,7 +2041,7 @@ export class BaseResolversVisitor< typeName, }); - const possibleTypes = implementingTypes.map(name => `'${name}'`).join(' | ') || 'null'; + const possibleTypes = implementingTypes.map(name => `'${name}'`).join(this.typeUnionOperator) || 'null'; // An Interface has __resolveType resolver, and no other fields. const blockFields: string[] = [ diff --git a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts index a37350bf7a6..da2a9211132 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts @@ -5,6 +5,7 @@ import { EnumValueDefinitionNode, FieldDefinitionNode, GraphQLEnumType, + GraphQLInterfaceType, GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, @@ -713,7 +714,7 @@ export class BaseTypesVisitor< const originalNode = parent[key] as UnionTypeDefinitionNode; const possibleTypes = originalNode.types .map(t => (this.scalars[t.name.value] ? this._getScalar(t.name.value, 'output') : this.convertName(t))) - .join(' | '); + .join(this.typeUnionOperator); return new DeclarationBlock(this._declarationBlockConfig) .export() @@ -732,22 +733,20 @@ export class BaseTypesVisitor< block.withBlock(this.mergeAllFields(fields, interfaces.length > 0)); } + getTypenameField(type: DeclarationKind, typeNames: string[]) { + const optionalTypename = this.config.nonOptionalTypename ? '__typename' : '__typename?'; + return `${this.config.immutableTypes ? 'readonly ' : ''}${optionalTypename}: ${typeNames + .map(typeName => `'${typeName}'`) + .join(this.typeUnionOperator)}${this.getPunctuation(type)}`; + } + getObjectTypeDeclarationBlock( node: ObjectTypeDefinitionNode, originalNode: ObjectTypeDefinitionNode ): DeclarationBlock { - const optionalTypename = this.config.nonOptionalTypename ? '__typename' : '__typename?'; const { type, interface: interfacesType } = this._parsedConfig.declarationKind; const allFields = [ - ...(this.config.addTypename - ? [ - indent( - `${this.config.immutableTypes ? 'readonly ' : ''}${optionalTypename}: '${ - node.name.value - }'${this.getPunctuation(type)}` - ), - ] - : []), + ...(this.config.addTypename ? [indent(this.getTypenameField(type, [node.name.value]))] : []), ...node.fields, ] as string[]; const interfacesNames = originalNode.interfaces ? originalNode.interfaces.map(i => this.convertName(i)) : []; @@ -790,13 +789,28 @@ export class BaseTypesVisitor< node: InterfaceTypeDefinitionNode, _originalNode: InterfaceTypeDefinitionNode ): DeclarationBlock { + const { type, interface: interfacesType } = this._parsedConfig.declarationKind; const declarationBlock = new DeclarationBlock(this._declarationBlockConfig) .export() - .asKind(this._parsedConfig.declarationKind.interface) + .asKind(interfacesType) .withName(this.convertName(node)) .withComment(node.description?.value); - return declarationBlock.withBlock(node.fields.join('\n')); + const schemaType = this._schema.getType(node.name.value); + const { objects: concreteTypes } = this._schema.getImplementations(schemaType as GraphQLInterfaceType); + const allFields = [ + ...node.fields, + ...(this.config.addTypenameToInterfaces && concreteTypes.length > 0 + ? [ + this.getTypenameField( + type, + concreteTypes.map(({ name }) => name) + ), + ] + : []), + ]; + + return declarationBlock.withBlock(allFields.join('\n')); } InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode, key: number | string, parent: any): string { diff --git a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts index 45903d52576..1703edc2fda 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts @@ -28,6 +28,7 @@ export interface ParsedConfig { typesPrefix: string; typesSuffix: string; addTypename: boolean; + addTypenameToInterfaces: boolean; nonOptionalTypename: boolean; extractAllFieldsToTypes: boolean; externalFragments: LoadedFragment[]; @@ -283,6 +284,28 @@ export interface RawConfig { * ``` */ skipTypename?: boolean; + /** + * @description Similarly to the `addTypename` option, if true, a `__typename` field will be added to type definitions resulting from interface declarations. + * + * @exampleMarkdown + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file': { + * // plugins... + * config: { + * addTypenameToInterfaces: true + * }, + * }, + * }, + * }; + * export default config; + * ``` + */ + addTypenameToInterfaces?: boolean; /** * @default false * @description Automatically adds `__typename` field to the generated types, even when they are not specified @@ -416,6 +439,7 @@ export class BaseVisitor 0) { - return implementingTypes.join(' | '); + return implementingTypes.join(this.typeUnionOperator); } } @@ -255,7 +255,7 @@ export class TsVisitor< const possibleTypes = originalNode.types .map(t => (this.scalars[t.name.value] ? this._getScalar(t.name.value, 'output') : this.convertName(t))) .concat(...withFutureAddedValue) - .join(' | '); + .join(this.typeUnionOperator); return new DeclarationBlock(this._declarationBlockConfig) .export() diff --git a/packages/plugins/typescript/typescript/tests/typescript.spec.ts b/packages/plugins/typescript/typescript/tests/typescript.spec.ts index f829c5e2550..100a8b9f548 100644 --- a/packages/plugins/typescript/typescript/tests/typescript.spec.ts +++ b/packages/plugins/typescript/typescript/tests/typescript.spec.ts @@ -3950,6 +3950,37 @@ describe('TypeScript', () => { `); }); + it('should union concrete type names in interface __typename field - issue #10522', async () => { + debugger; + const testSchema = buildSchema(/* GraphQL */ ` + interface TopLevel { + topLevelField: Boolean + } + + type OneImplementation implements TopLevel { + topLevelField: Boolean + implementationField: String + } + + type AnotherImplementation implements TopLevel { + topLevelField: Boolean + anotherImplementationField: Int + } + `); + const output = await plugin( + testSchema, + [], + { + addTypenameToInterfaces: true, + }, + { outputFile: 'graphql.ts' } + ); + + expect(output.content).toBeSimilarStringTo(` + __typename?: 'OneImplementation' | 'AnotherImplementation' + `); + }); + it('should use implementing types as node type - issue #5126', async () => { const testSchema = buildSchema(/* GraphQL */ ` type Matrix {