Skip to content

Commit 4ef6503

Browse files
committed
Only generate Exact utility type if documents have operations (#10571)
* Remove extraneous test setup * Do not generate Exact utility type of not used * Add default test case to check that Exact is used for Mutation and Subscription variables * Update dev-tests * Add changeset * Remove extraneous config for Exact test
1 parent 2db890c commit 4ef6503

5 files changed

Lines changed: 93 additions & 15 deletions

File tree

.changeset/khaki-clubs-say.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@graphql-codegen/typescript-operations': patch
3+
---
4+
5+
Only generate `Exact` utility type at the top if it is used
6+
7+
`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.
8+
9+
This is important to conditionally generate as users may use very strict tsconfig that will fail compiling if there are unused types.

dev-test/star-wars/types.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
21
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
7373
TypeScriptDocumentsParsedConfig
7474
> {
7575
protected _usedNamedInputTypes: UsedNamedInputTypes = {};
76+
protected _needsExactUtilityType: boolean = false;
7677
private _outputPath: string;
7778

7879
constructor(
@@ -386,7 +387,7 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
386387

387388
protected applyVariablesWrapper(variablesBlock: string, operationType: string): string {
388389
const extraType = this.config.allowUndefinedQueryVariables && operationType === 'Query' ? ' | undefined' : '';
389-
390+
this._needsExactUtilityType = true;
390391
return `Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`;
391392
}
392393

@@ -501,7 +502,10 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
501502
}
502503

503504
getExactUtilityType(): string | null {
504-
if (!this.config.generatesOperationTypes) {
505+
if (
506+
!this.config.generatesOperationTypes || // 1. If we don't generate operation types, definitely do not need `Exact`
507+
!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
508+
) {
505509
return null;
506510
}
507511

packages/plugins/typescript/operations/tests/ts-documents.standalone.spec.ts

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ describe('TypeScript Operations Plugin - Standalone', () => {
1111
users(input: UsersInput!): UsersResponse!
1212
}
1313
14+
type Mutation {
15+
makeUserAdmin(id: ID!): User!
16+
}
17+
18+
type Subscription {
19+
userChanges(id: ID!): User!
20+
}
21+
1422
type ResponseError {
1523
error: ResponseErrorType!
1624
}
@@ -90,6 +98,23 @@ describe('TypeScript Operations Plugin - Standalone', () => {
9098
}
9199
}
92100
}
101+
102+
mutation MakeAdmin {
103+
makeUserAdmin(id: "100") {
104+
...UserFragment
105+
}
106+
}
107+
108+
subscription UserChanges {
109+
makeUserAdmin(id: "100") {
110+
...UserFragment
111+
}
112+
}
113+
114+
fragment UserFragment on User {
115+
id
116+
role
117+
}
93118
`);
94119

95120
const result = mergeOutputs([await plugin(schema, [{ document }], {}, { outputFile: '' })]);
@@ -147,6 +172,18 @@ describe('TypeScript Operations Plugin - Standalone', () => {
147172
| { result: Array<{ __typename: 'User' }> }
148173
| { __typename: 'ResponseError' }
149174
};
175+
176+
export type MakeAdminMutationVariables = Exact<{ [key: string]: never; }>;
177+
178+
179+
export type MakeAdminMutation = { makeUserAdmin: { id: string, role: UserRole } };
180+
181+
export type UserChangesSubscriptionVariables = Exact<{ [key: string]: never; }>;
182+
183+
184+
export type UserChangesSubscription = Record<PropertyKey, never>;
185+
186+
export type UserFragmentFragment = { id: string, role: UserRole };
150187
"
151188
`);
152189

@@ -712,17 +749,6 @@ describe('TypeScript Operations Plugin - Standalone', () => {
712749
user(id: ID!): User
713750
}
714751
715-
type ResponseError {
716-
error: ResponseErrorType!
717-
}
718-
719-
enum ResponseErrorType {
720-
NOT_FOUND
721-
INPUT_VALIDATION_ERROR
722-
FORBIDDEN_ERROR
723-
UNEXPECTED_ERROR
724-
}
725-
726752
type User {
727753
id: ID!
728754
name: String!
@@ -782,4 +808,45 @@ describe('TypeScript Operations Plugin - Standalone', () => {
782808

783809
validateTs(result, undefined, undefined, undefined, undefined, true);
784810
});
811+
812+
it('does not generate Exact utility type if there are only fragments', async () => {
813+
const schema = buildSchema(/* GraphQL */ `
814+
type Query {
815+
user(id: ID!): User
816+
}
817+
818+
type User {
819+
id: ID!
820+
name: String!
821+
role: UserRole!
822+
createdAt: DateTime!
823+
bestFriend: User
824+
goodFriends: [User!]!
825+
}
826+
827+
enum UserRole {
828+
ADMIN
829+
CUSTOMER
830+
}
831+
832+
scalar DateTime
833+
`);
834+
const document = parse(/* GraphQL */ `
835+
fragment UserPart on User {
836+
id
837+
name
838+
}
839+
`);
840+
841+
const result = mergeOutputs([await plugin(schema, [{ document }], {}, { outputFile: '' })]);
842+
843+
expect(result).toMatchInlineSnapshot(`
844+
"
845+
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
846+
export type UserPartFragment = { id: string, name: string };
847+
"
848+
`);
849+
850+
validateTs(result, undefined, undefined, undefined, undefined, true);
851+
});
785852
});

packages/presets/client/tests/client-preset.enum.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ describe('client-preset - Enum', () => {
2323
const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts');
2424
expect(graphqlFile.content).toMatchInlineSnapshot(`
2525
"/* eslint-disable */
26-
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
2726
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };"
2827
`);
2928
});

0 commit comments

Comments
 (0)