-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[typescript-resolvers] Extract union types to ResolversUnionTypes #9069
[typescript-resolvers] Extract union types to ResolversUnionTypes #9069
Conversation
🦋 Changeset detectedLatest commit: 7758126 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts
Outdated
Show resolved
Hide resolved
@@ -880,6 +876,82 @@ export class BaseResolversVisitor< | |||
return `Array<${t}>`; | |||
} | |||
|
|||
protected createResolversUnionTypes(): Record<string, string> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createResolversUnionTypes
is the simplified version of createResolversFields
that is targeted for Unions.
I chose to create a new function because createResolversFields
is quite large and rigid so it's hard to implement/maintain
packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts
Outdated
Show resolved
Hide resolved
|
||
return unionMemberValue; | ||
}); | ||
res[typeName] = referencedTypes.map(type => `( ${type} )`).join(' | '); // Must wrap every union member in explicit "( )" to separate the members |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must wrap every union member in explicit "( )" to separate the members
Result usually looks like this:
export type ResolversUnionTypes = {
UnionThing: ( TypeA ) | ( TypeB )
}
Prettier should remove the round brackets automatically i.e. result will be UnionThing: TypeA | TypeB
I've added it this way so it's a bit easier to add types to the union members without ambiguity e.g.
// This is ambiguous
UnionThing: TypeA & { __typename: 'TypeA' } | TypeB & { __typename: 'TypeB' }
// Prettier will try to turn it into this:
UnionThing: ( TypeA & { __typename: 'TypeA' } ) | ( TypeB & { __typename: 'TypeB' } )
@@ -1426,10 +1426,38 @@ export type ResolverFn<TResult, TParent, TContext, TArgs> = ( | |||
union CCCUnion = CCCFoo | CCCBar | |||
`); | |||
|
|||
const tsContent = (await tsPlugin(testSchema, [], {}, { outputFile: 'graphql.ts' })) as Types.ComplexPluginOutput; | |||
const tsContent = await tsPlugin(testSchema, [], {}, { outputFile: 'graphql.ts' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tsContent
should be typed as Types.ComplexPluginOutput
so we don't need to do as Types.ComplexPluginOutput
.
Happy to put this back if it was here for some particular reasons
expect(result.content).toBeSimilarStringTo(` | ||
export type ResolversParentTypes = { | ||
MyType: Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversParentTypes['ChildUnion']> }; | ||
String: Scalars['String']; | ||
Child: Child; | ||
MyOtherType: MyOtherType; | ||
ChildUnion: ResolversUnionTypes['ChildUnion']; | ||
Query: {}; | ||
Subscription: {}; | ||
Node: ResolversParentTypes['SomeNode']; | ||
ID: Scalars['ID']; | ||
SomeNode: SomeNode; | ||
MyUnion: ResolversUnionTypes['MyUnion']; | ||
MyScalar: Scalars['MyScalar']; | ||
Int: Scalars['Int']; | ||
Boolean: Scalars['Boolean']; | ||
}; | ||
`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added ResolversParentTypes
to all related tests in this file because I want to be sure that ResolversUnionTypes
is being used correctly to reference.
expect(result.content).toBeSimilarStringTo(` | ||
export type ResolversTypes = { | ||
MyType: ResolverTypeWrapper<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> }>; | ||
String: ResolverTypeWrapper<Scalars['String']>; | ||
Child: ResolverTypeWrapper<Child>; | ||
MyOtherType: ResolverTypeWrapper<MyOtherType>; | ||
ChildUnion: ResolversTypes['Child'] | ResolversTypes['MyOtherType']; | ||
ChildUnion: ResolverTypeWrapper<ResolversUnionTypes['ChildUnion']>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, this is ResolversTypes['Child'] | ResolversTypes['MyOtherType']
which translate to:
ResolverTypeWrapper<Child> | ResolverTypeWrapper<MyOtherType>
Now, using ResolverTypeWrapper<ResolversUnionTypes['ChildUnion']>
translate to:
ResolverTypeWrapper<Child | MyOtherType>
I think these 2 are the same semantically. Happy to hear explanation if they are not 🙂
(This is applicable to all updated tests in this file)
expect(result.content).toBeSimilarStringTo(` | ||
export type ResolversUnionTypes = { | ||
ChildUnion: ( Omit<Child, 'parent'> & { parent?: Maybe<ResolversTypes['MyType']> } ) | ( MyOtherType ); | ||
MyUnion: ( CustomPartial<Omit<MyType, 'unionChild'> & { unionChild?: Maybe<ResolversTypes['ChildUnion']> }> ) | ( MyOtherType ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be related to #9043 as well.
Ideally, the MyType
is should be treated the same way as ResolversParentTypes['MyType']
. However:
- The
ResolversParentTypes['MyType']
is using the type from typescript plugin. - The mentioned issue seems to suggest that we should use the "Omit type" instead
Currently, ResolversUnionTypes
is using "Omit type" to replace the { T }
placeholder here but happy to change to match whichever direction ResolversParentTypes['MyType']
uses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eddeee888 awesome work!
I implemented a fix here #9110 , but it's conflicting. I'll adjust rebase it on top of yours after it's merged
a65e065
to
4291915
Compare
const originalTypeName = isScalar ? this._getScalar(typeName) : prev[typeName]; | ||
|
||
// Don't wrap Union with ResolverTypeWrapper, each inner type already has it | ||
if (isUnionType(schemaType)) { | ||
prev[typeName] = replaced; | ||
// Don't clear ResolverTypeWrapper from Unions | ||
prev[typeName] = replacePlaceholder(this.config.defaultMapper.type, originalTypeName); | ||
} else { | ||
prev[typeName] = applyWrapper(replacePlaceholder(this.config.defaultMapper.type, name)); | ||
const name = clearWrapper(originalTypeName); | ||
const replaced = replacePlaceholder(this.config.defaultMapper.type, name); | ||
prev[typeName] = applyWrapper(replaced); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previous logic suggests that it's possible for a union to be in the scalar map 🤔 I'm unsure when/if it's possible.
However, the updated logic should be backward compatible as it generates the same partial and wrapper order.
…pes using this.convertName
Since unions are now treated separately, instead of inline referenced, we do not need to avoid wrapping ResolverTypeWrapper. However, the result now is ResolverTypeWrapper<Partial< A | B >> instead of: Partial<ResolverTypeWrapper<A> | ResolverTypeWrapper<B> > But... maybe it's ok? 🤔
89a4288
to
09b4d59
Compare
This caused an issue in my project due to the fact that now the first argument for |
|
you can also extract all the sub-types as well. import type { ResolversTypes } from "./path/to/generated/graphql"
type ShallowTypes = {
[Key in keyof ResolversTypes]: Exclude<ResolversTypes[Key], Promise<unknown>>
} |
@ryanrhee thanks for your answer. The issue is with the parent argument (first argument) of the resolver for
I guess the only way to fix it is to add this type to the mappers as well? |
You could add it to the mappers, yes. Does {
__resolveType: async (objOrPromise, ...) => {
// objOrPromise is `Promise<T> | T`
const obj = Promise.resolve(objOrPromise)
// your code here; obj is now of type `T`
}
} |
Thanks @ryanrhee. I needed to add
However, it is a bit concerning that this happened in a patch update from |
Hi @hector-del-rio , Do you mind creating a small repo or sandbox with a small schema and minimal codegen config? This will help me understand the problem better 🙂 For context, this went in as a patch because we only move type references around. Maybe I missed a use case. Sorry! |
Hi @hector-del-rio, This should be fixed in #9206 🙂 |
Thank you @eddeee888 🙏 |
Description
As discussed in a proof of concept, we thought that by creating a
ResolversUnionTypes
type and use reference unions inResolversTypes
andResolversParentTypes
give us a bit more control in the future.As the result, the union types are no longer inlined in
ResolversTypes
andResolversParentTypes
Example use case (future)
In the future, we could make union members'
__typename
non-nullable whilst leaving the counterparts inResolversTypes
andResolversParentTypes
as optional:Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Test Environment:
@graphql-codegen/typescript-resolvers
:Checklist: