-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[typescript-resolvers] Extract union types to ResolversUnionTypes #9069
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
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
Show resolved
Hide resolved
| 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' } ) | `); | ||
|
|
||
| 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.
| 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
ResolversUnionTypestype and use reference unions inResolversTypesandResolversParentTypesgive us a bit more control in the future.As the result, the union types are no longer inlined in
ResolversTypesandResolversParentTypesExample use case (future)
In the future, we could make union members'
__typenamenon-nullable whilst leaving the counterparts inResolversTypesandResolversParentTypesas optional:Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Test Environment:
@graphql-codegen/typescript-resolvers:Checklist: