Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .changeset/khaki-clubs-say.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 0 additions & 1 deletion dev-test/star-wars/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
8 changes: 6 additions & 2 deletions packages/plugins/typescript/operations/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
TypeScriptDocumentsParsedConfig
> {
protected _usedNamedInputTypes: UsedNamedInputTypes = {};
protected _needsExactUtilityType: boolean = false;
private _outputPath: string;

constructor(
Expand Down Expand Up @@ -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}`;
}

Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!
}
Expand Down Expand Up @@ -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
}
Comment on lines +102 to +117
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding these to make sure we test many things in the default test.
This gives more confidence overall.
e.g. in this PR I want to make sure Exact is used for mutation and subscription variables.

`);

const result = mergeOutputs([await plugin(schema, [{ document }], {}, { outputFile: '' })]);
Expand Down Expand Up @@ -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<PropertyKey, never>;

export type UserFragmentFragment = { id: string, role: UserRole };
"
`);

Expand Down Expand Up @@ -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
}
Comment on lines -715 to -724
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extraneous and never used in this test, so removed.


type User {
id: ID!
name: String!
Expand Down Expand Up @@ -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> = 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);
});
});
1 change: 0 additions & 1 deletion packages/presets/client/tests/client-preset.enum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };"
`);
});
Expand Down