Skip to content

Commit 9f4d9c5

Browse files
authored
[typescript-resolvers] Add resolversNonOptionalTypename config (#9146)
1 parent b7dacb2 commit 9f4d9c5

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

Diff for: .changeset/spicy-worms-jam.md

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': minor
3+
'@graphql-codegen/typescript-resolvers': minor
4+
---
5+
6+
[typescript-resolvers] Add `resolversNonOptionalTypename` config option.
7+
8+
This is extending on `ResolversUnionTypes` implemented in https://github.com/dotansimha/graphql-code-generator/pull/9069
9+
10+
`resolversNonOptionalTypename` adds non-optional `__typename` to union members of `ResolversUnionTypes`, without affecting the union members' base intefaces.
11+
12+
A common use case for non-optional `__typename` of union members is using it as the common field to work out the final schema type. This makes implementing the union's `__resolveType` very simple as we can use `__typename` to decide which union member the resolved object is. Without this, we have to check the existence of field/s on the incoming object which could be verbose.
13+
14+
For example, consider this schema:
15+
16+
```graphql
17+
type Query {
18+
book(id: ID!): BookPayload!
19+
}
20+
21+
type Book {
22+
id: ID!
23+
isbn: String!
24+
}
25+
26+
type BookResult {
27+
node: Book
28+
}
29+
30+
type PayloadError {
31+
message: String!
32+
}
33+
34+
union BookPayload = BookResult | PayloadError
35+
```
36+
37+
*With optional `__typename`:* We need to check existence of certain fields to resolve type in the union resolver:
38+
39+
```ts
40+
// Query/book.ts
41+
export const book = async () => {
42+
try {
43+
const book = await fetchBook();
44+
// 1. No `__typename` in resolver results...
45+
return {
46+
node: book
47+
}
48+
} catch(e) {
49+
return {
50+
message: "Failed to fetch book"
51+
}
52+
}
53+
}
54+
55+
// BookPayload.ts
56+
export const BookPayload = {
57+
__resolveType: (parent) => {
58+
// 2. ... means more checks in `__resolveType`
59+
if('message' in parent) {
60+
return 'PayloadError';
61+
}
62+
return 'BookResult'
63+
}
64+
}
65+
```
66+
67+
*With non-optional `__typename`:* Resolvers declare the type. This which gives us better TypeScript support in resolvers and simplify `__resolveType` implementation:
68+
69+
```ts
70+
// Query/book.ts
71+
export const book = async () => {
72+
try {
73+
const book = await fetchBook();
74+
// 1. `__typename` is declared in resolver results...
75+
return {
76+
__typename: 'BookResult', // 1a. this also types `node` for us 🎉
77+
node: book
78+
}
79+
} catch(e) {
80+
return {
81+
__typename: 'PayloadError',
82+
message: "Failed to fetch book"
83+
}
84+
}
85+
}
86+
87+
// BookPayload.ts
88+
export const BookPayload = {
89+
__resolveType: (parent) => parent.__typename, // 2. ... means a very simple check in `__resolveType`
90+
}
91+
```
92+
93+
*Using `resolversNonOptionalTypename`:* add it into `typescript-resolvers` plugin config:
94+
95+
```ts
96+
// codegen.ts
97+
const config: CodegenConfig = {
98+
schema: 'src/schema/**/*.graphql',
99+
generates: {
100+
'src/schema/types.ts': {
101+
plugins: ['typescript', 'typescript-resolvers'],
102+
config: {
103+
resolversNonOptionalTypename: true // Or `resolversNonOptionalTypename: { unionMember: true }`
104+
}
105+
},
106+
},
107+
};
108+
```

Diff for: packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
EnumValuesMap,
3636
NormalizedScalarsMap,
3737
ParsedEnumValuesMap,
38+
ResolversNonOptionalTypenameConfig,
3839
} from './types.js';
3940
import {
4041
buildScalarsFromConfig,
@@ -73,6 +74,7 @@ export interface ParsedResolversConfig extends ParsedConfig {
7374
internalResolversPrefix: string;
7475
onlyResolveTypeForInterfaces: boolean;
7576
directiveResolverMappings: Record<string, string>;
77+
resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig;
7678
}
7779

7880
export interface RawResolversConfig extends RawConfig {
@@ -537,6 +539,11 @@ export interface RawResolversConfig extends RawConfig {
537539
* @description Turning this flag to `true` will generate resolver signature that has only `resolveType` for interfaces, forcing developers to write inherited type resolvers in the type itself.
538540
*/
539541
onlyResolveTypeForInterfaces?: boolean;
542+
/**
543+
* @description Makes `__typename` of resolver mappings non-optional without affecting the base types.
544+
* @default false
545+
*/
546+
resolversNonOptionalTypename?: boolean | ResolversNonOptionalTypenameConfig;
540547
/**
541548
* @ignore
542549
*/
@@ -604,6 +611,9 @@ export class BaseResolversVisitor<
604611
mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix),
605612
scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars),
606613
internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'),
614+
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
615+
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
616+
),
607617
...additionalConfig,
608618
} as TPluginConfig);
609619

@@ -899,7 +909,11 @@ export class BaseResolversVisitor<
899909
return replacePlaceholder(this.config.defaultMapper.type, finalTypename);
900910
}
901911

902-
return unionMemberValue;
912+
const nonOptionalTypenameModifier = this.config.resolversNonOptionalTypename.unionMember
913+
? ` & { __typename: "${unionMemberType}" }`
914+
: '';
915+
916+
return `${unionMemberValue}${nonOptionalTypenameModifier}`;
903917
});
904918
res[typeName] = referencedTypes.map(type => `( ${type} )`).join(' | '); // Must wrap every union member in explicit "( )" to separate the members
905919
}
@@ -1583,3 +1597,22 @@ function replacePlaceholder(pattern: string, typename: string): string {
15831597
function hasPlaceholder(pattern: string): boolean {
15841598
return pattern.includes('{T}');
15851599
}
1600+
1601+
function normalizeResolversNonOptionalTypename(
1602+
input?: boolean | ResolversNonOptionalTypenameConfig
1603+
): ResolversNonOptionalTypenameConfig {
1604+
const defaultConfig: ResolversNonOptionalTypenameConfig = {
1605+
unionMember: false,
1606+
};
1607+
1608+
if (typeof input === 'boolean') {
1609+
return {
1610+
unionMember: input,
1611+
};
1612+
}
1613+
1614+
return {
1615+
...defaultConfig,
1616+
...input,
1617+
};
1618+
}

Diff for: packages/plugins/other/visitor-plugin-common/src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,7 @@ export interface ParsedImport {
102102
moduleName: string | null;
103103
propName: string;
104104
}
105+
106+
export interface ResolversNonOptionalTypenameConfig {
107+
unionMember?: boolean;
108+
}

Diff for: packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts

+32
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,38 @@ export type MyTypeResolvers<ContextType = any, ParentType extends ResolversParen
224224

225225
await resolversTestingValidate(result);
226226
});
227+
228+
it('resolversNonOptionalTypename - adds non-optional typenames to implemented types', async () => {
229+
const result = await plugin(
230+
resolversTestingSchema,
231+
[],
232+
{ resolversNonOptionalTypename: true },
233+
{ outputFile: '' }
234+
);
235+
236+
expect(result.content).toBeSimilarStringTo(`
237+
export type ResolversUnionTypes = {
238+
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
239+
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
240+
};
241+
`);
242+
});
243+
244+
it('resolversNonOptionalTypename - adds non-optional typenames to ResolversUnionTypes', async () => {
245+
const result = await plugin(
246+
resolversTestingSchema,
247+
[],
248+
{ resolversNonOptionalTypename: { unionMember: true } },
249+
{ outputFile: '' }
250+
);
251+
252+
expect(result.content).toBeSimilarStringTo(`
253+
export type ResolversUnionTypes = {
254+
ChildUnion: ( Child & { __typename: "Child" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
255+
MyUnion: ( Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> } & { __typename: "MyType" } ) | ( MyOtherType & { __typename: "MyOtherType" } );
256+
};
257+
`);
258+
});
227259
});
228260

229261
it('directiveResolverMappings - should generate correct types (import definition)', async () => {

0 commit comments

Comments
 (0)