Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,9 @@ export class BaseResolversVisitor<
relevantFields: ReturnType<typeof this.getRelevantFieldsToOmit>
): 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(', ')} }`;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 + '<ContextType>',
Expand Down Expand Up @@ -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[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EnumValueDefinitionNode,
FieldDefinitionNode,
GraphQLEnumType,
GraphQLInterfaceType,
GraphQLSchema,
InputObjectTypeDefinitionNode,
InputValueDefinitionNode,
Expand Down Expand Up @@ -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()
Expand All @@ -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)) : [];
Expand Down Expand Up @@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions packages/plugins/other/visitor-plugin-common/src/base-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface ParsedConfig {
typesPrefix: string;
typesSuffix: string;
addTypename: boolean;
addTypenameToInterfaces: boolean;
nonOptionalTypename: boolean;
extractAllFieldsToTypes: boolean;
externalFragments: LoadedFragment[];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -416,6 +439,7 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
externalFragments: rawConfig.externalFragments || [],
fragmentImports: rawConfig.fragmentImports || [],
addTypename: !rawConfig.skipTypename,
addTypenameToInterfaces: !rawConfig.skipTypename && rawConfig.addTypenameToInterfaces,
nonOptionalTypename: !!rawConfig.nonOptionalTypename,
useTypeImports: !!rawConfig.useTypeImports,
allowEnumStringTypes: !!rawConfig.allowEnumStringTypes,
Expand Down Expand Up @@ -451,6 +475,10 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
return this._parsedConfig;
}

get typeUnionOperator() {
return ' | ';
}

public convertName(node: ASTNode | string, options?: BaseVisitorConvertOptions & ConvertOptions): string {
const useTypesPrefix = typeof options?.useTypesPrefix === 'boolean' ? options.useTypesPrefix : true;
const useTypesSuffix = typeof options?.useTypesSuffix === 'boolean' ? options.useTypesSuffix : true;
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/typescript/typescript/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class TsVisitor<
}

if (implementingTypes.length > 0) {
return implementingTypes.join(' | ');
return implementingTypes.join(this.typeUnionOperator);
}
}

Expand Down Expand Up @@ -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()
Expand Down
31 changes: 31 additions & 0 deletions packages/plugins/typescript/typescript/tests/typescript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down