diff --git a/.changeset/khaki-clubs-say.md b/.changeset/khaki-clubs-say.md new file mode 100644 index 00000000000..0bbae4c9c99 --- /dev/null +++ b/.changeset/khaki-clubs-say.md @@ -0,0 +1,9 @@ +--- +'@graphql-codegen/typescript-operations': patch +--- + +Only generate `Exact` utility type at the top if it is used + +`Exact` utility is only used to wrap variables types for operations (queries, mutations and subscriptions) if they exist in the document. `Exact` is never used when there are _only_ fragments. + +This is important to conditionally generate as users may use very strict tsconfig that will fail compiling if there are unused types. diff --git a/dev-test/star-wars/types.d.ts b/dev-test/star-wars/types.d.ts index 0fd39f72102..ef961c94a56 100644 --- a/dev-test/star-wars/types.d.ts +++ b/dev-test/star-wars/types.d.ts @@ -1,2 +1 @@ -type Exact = { [K in keyof T]: T[K] }; export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; diff --git a/packages/plugins/typescript/operations/src/visitor.ts b/packages/plugins/typescript/operations/src/visitor.ts index 2c0085263ad..1089c669ba5 100644 --- a/packages/plugins/typescript/operations/src/visitor.ts +++ b/packages/plugins/typescript/operations/src/visitor.ts @@ -73,6 +73,7 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< TypeScriptDocumentsParsedConfig > { protected _usedNamedInputTypes: UsedNamedInputTypes = {}; + protected _needsExactUtilityType: boolean = false; private _outputPath: string; constructor( @@ -386,7 +387,7 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< protected applyVariablesWrapper(variablesBlock: string, operationType: string): string { const extraType = this.config.allowUndefinedQueryVariables && operationType === 'Query' ? ' | undefined' : ''; - + this._needsExactUtilityType = true; return `Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`; } @@ -501,7 +502,10 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< } getExactUtilityType(): string | null { - if (!this.config.generatesOperationTypes) { + if ( + !this.config.generatesOperationTypes || // 1. If we don't generate operation types, definitely do not need `Exact` + !this._needsExactUtilityType // 2. Even if we generate operation types, we may not need `Exact` if there's no operations in the documents i.e. only fragments found + ) { return null; } diff --git a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts index 466b11ce8fd..91ed3b431c4 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts @@ -11,6 +11,14 @@ describe('TypeScript Operations Plugin - Standalone', () => { users(input: UsersInput!): UsersResponse! } + type Mutation { + makeUserAdmin(id: ID!): User! + } + + type Subscription { + userChanges(id: ID!): User! + } + type ResponseError { error: ResponseErrorType! } @@ -90,6 +98,23 @@ describe('TypeScript Operations Plugin - Standalone', () => { } } } + + mutation MakeAdmin { + makeUserAdmin(id: "100") { + ...UserFragment + } + } + + subscription UserChanges { + makeUserAdmin(id: "100") { + ...UserFragment + } + } + + fragment UserFragment on User { + id + role + } `); const result = mergeOutputs([await plugin(schema, [{ document }], {}, { outputFile: '' })]); @@ -147,6 +172,18 @@ describe('TypeScript Operations Plugin - Standalone', () => { | { result: Array<{ __typename: 'User' }> } | { __typename: 'ResponseError' } }; + + export type MakeAdminMutationVariables = Exact<{ [key: string]: never; }>; + + + export type MakeAdminMutation = { makeUserAdmin: { id: string, role: UserRole } }; + + export type UserChangesSubscriptionVariables = Exact<{ [key: string]: never; }>; + + + export type UserChangesSubscription = Record; + + export type UserFragmentFragment = { id: string, role: UserRole }; " `); @@ -712,17 +749,6 @@ describe('TypeScript Operations Plugin - Standalone', () => { user(id: ID!): User } - type ResponseError { - error: ResponseErrorType! - } - - enum ResponseErrorType { - NOT_FOUND - INPUT_VALIDATION_ERROR - FORBIDDEN_ERROR - UNEXPECTED_ERROR - } - type User { id: ID! name: String! @@ -782,4 +808,45 @@ describe('TypeScript Operations Plugin - Standalone', () => { validateTs(result, undefined, undefined, undefined, undefined, true); }); + + it('does not generate Exact utility type if there are only fragments', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + user(id: ID!): User + } + + type User { + id: ID! + name: String! + role: UserRole! + createdAt: DateTime! + bestFriend: User + goodFriends: [User!]! + } + + enum UserRole { + ADMIN + CUSTOMER + } + + scalar DateTime + `); + const document = parse(/* GraphQL */ ` + fragment UserPart on User { + id + name + } + `); + + const result = mergeOutputs([await plugin(schema, [{ document }], {}, { outputFile: '' })]); + + expect(result).toMatchInlineSnapshot(` + " + export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; + export type UserPartFragment = { id: string, name: string }; + " + `); + + validateTs(result, undefined, undefined, undefined, undefined, true); + }); }); diff --git a/packages/presets/client/tests/client-preset.enum.spec.ts b/packages/presets/client/tests/client-preset.enum.spec.ts index c6dcc1897a0..3e55808d024 100644 --- a/packages/presets/client/tests/client-preset.enum.spec.ts +++ b/packages/presets/client/tests/client-preset.enum.spec.ts @@ -23,7 +23,6 @@ describe('client-preset - Enum', () => { const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts'); expect(graphqlFile.content).toMatchInlineSnapshot(` "/* eslint-disable */ - type Exact = { [K in keyof T]: T[K] }; export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };" `); });