Skip to content

Conversation

@eddeee888
Copy link
Collaborator

@eddeee888 eddeee888 commented Dec 16, 2024

Related: #10413, #7020, #10385, #9701

typescript-resolvers & Federation Changes

Related: #10206

Breaking Changes

  • No longer generate UnwrappedObject utility type, as this was used to support the wrong previously generated type.
  • Deprecate onlyResolveTypeForInterfaces because majority of use cases cannot implement resolvers in Interfaces.
  • Deprecate generateInternalResolversIfNeeded.__resolveReference because types do not have __resolveReference if they are not Federation entities or are not resolvable. Users should not have to manually set this option. This option was put in to wait for this major version.
  • Do not generate __isTypeOf for non-implementing-types or non-union-members
  • Do not inline parent types for Federation Entities resolvers anymore. This makes handling mappers and deciding @external, @provides and @key @key scenarios easier

Refactors

Breaking changes

  • Record<PropertyKey, never> is used instead of {} for empty object type in @graphql-codegen/typescript and @graphql-codegen/typescript-resolvers

Client Preset

- [ ] Improve default and forwarded config Will happen after major version update

@changeset-bot
Copy link

changeset-bot bot commented Dec 16, 2024

🦋 Changeset detected

Latest commit: 8874b85

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@graphql-codegen/visitor-plugin-common Major
@graphql-codegen/typescript-resolvers Major
@graphql-codegen/plugin-helpers Major
@graphql-codegen/cli Major
@graphql-codegen/testing Major
@graphql-codegen/typescript Major
@graphql-codegen/typescript-operations Major
@graphql-codegen/client-preset Major
@graphql-codegen/graphql-modules-preset Major
@graphql-codegen/core Major
@graphql-codegen/add Major
@graphql-codegen/fragment-matcher Major
@graphql-codegen/introspection Major
@graphql-codegen/schema-ast Major
@graphql-codegen/time Major
@graphql-codegen/typescript-document-nodes Major
@graphql-codegen/gql-tag-operations Major
@graphql-codegen/typed-document-node Major

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

@github-actions
Copy link
Contributor

github-actions bot commented Dec 16, 2024

💻 Website Preview

The latest changes are available as preview in: https://pr-10218.graphql-code-generator.pages.dev

@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2025

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
@graphql-codegen/cli 6.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/core 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/add 6.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/fragment-matcher 6.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/introspection 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/schema-ast 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/time 6.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/visitor-plugin-common 6.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-document-nodes 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/gql-tag-operations 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-operations 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-resolvers 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/typed-document-node 6.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/client-preset 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/graphql-modules-preset 5.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/testing 4.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎
@graphql-codegen/plugin-helpers 6.0.0-alpha-20250907025311-8874b85f4c8e523c94bf7f20a7544c353bc1414a npm ↗︎ unpkg ↗︎

@eddeee888 eddeee888 changed the title Federation feature branch Next major version feature branch Jan 28, 2025
@eddeee888 eddeee888 force-pushed the federation-fixes branch 2 times, most recently from fd51b95 to 56fea45 Compare January 29, 2025 09:27
@eddeee888 eddeee888 force-pushed the federation-fixes branch from 6a2bca7 to 30ac893 Compare May 9, 2025 13:40
@eddeee888 eddeee888 self-assigned this Jul 8, 2025
@eddeee888 eddeee888 mentioned this pull request Aug 13, 2025
1 task
…put meta (#10417)

* Report hasIsTypeOf in meta

* Add changeset

* Revert "Remove CI config used for dev"

This reverts commit 6d2c6e4.
@clemens
Copy link

clemens commented Sep 9, 2025

@eddeee888 What a huge set of changes – I appreciate the hard work that went into this over months!

I'm confused about one change here:

Deprecate onlyResolveTypeForInterfaces because majority of use cases cannot implement resolvers in Interfaces.

The confusion is for 3 reasons:

First of all, it seems to not have been deprecated but actually removed – so unless I'm missing something, there doesn't seem to be a way anymore to have anything except for __resolveType in interface resolvers.
=> While it's fine to introduce breaking changes in major versions (that's, among other things, what they're for), I'd argue that ideally we'd have had an actual deprecation in an intermittent minor release before removing it altogether.
=> Independent of this, I think at least the changelog entries should state that it's been removed without replacement rather than deprecated.


I'm also curious why a "majority of use cases cannot implement resolvers in Interfaces". I get that it's an age-old discussion whether it should be possible to share only structure or also code via interfaces; but it certainly is convenient, e.g. when you have different user types that might even be managed in the same exact data source (think: users table in a Postgres database) and want to share certain traits between them:

import type { UserInterfaceResolvers } from "./types"

export const UserInterface: UserInterfaceResolvers = {
  __resolveType: ({ __typename }) => __typename,
  isConfirmed: ({ user }) => user.confirmedAt !== null,
  // …and any other fields that might be shared between multiple user types
}
import type { UserResolvers } from "./types"

export const User: UserResolvers = {
  __resolveType: ({ __typename }) => __typename,
}
import type { AdminResolvers } from "./types"

export const Admin: AdminResolvers = {
  __resolveType: ({ __typename }) => __typename,
}

With the changes in this release, unless I'm overlooking something, I'd either have to copy & paste isConfirmed to both Admin and User resolvers (and any other sub types of users that I might have now or in the future) or extract it into some type of shared methods and import these everywhere.

And it only gets worse as number of types and shared behaviors increase; and then maybe the complexity increases as well (e.g. certain interfaces only apply to certain user types) and all of a sudden it turns into a mess.


Last but not least, I'm wondering how this is related to federation. As far as I can see, the types on the GraphQL side of things don't change: The UserInterface in the above example still has isConfirmed. The change seems to only be on TypeScript side, where I can't easily share the implementation of isConfirmed anymore between resolvers implementing the interface, even if it is exactly the same. But a pure implementation-related change shouldn't affect federation, which is implementation-agnostic, right?

Could you give an example where this is an issue in the context of federation?

@eddeee888
Copy link
Collaborator Author

eddeee888 commented Sep 9, 2025

Hi @clemens,

Thanks for the question and feedback!

I think at least the changelog entries should state that it's been removed without replacement rather than deprecated.

Apologies about the confusion in the term. In this context, it is meant to say the config is removed, as you suspect. I'll clarify this intention better in the future.


With the changes in this release, unless I'm overlooking something, I'd either have to copy & paste isConfirmed to both Admin and User resolvers (and any other sub types of users that I might have now or in the future) or extract it into some type of shared methods and import these everywhere.

I believe this is how shared field resolvers on an Interface are supposed to work. I've tested this many times and never observed field resolvers in Interface work:

import type { UserInterfaceResolvers } from "./types"

export const UserInterface: UserInterfaceResolvers = {
  __resolveType: ({ __typename }) => __typename,
  isConfirmed: ({ user }) => {
    console.log("Never called: " , { user });
    return user.confirmedAt !== null,
  }
}

import type { UserResolvers } from "./types"

export const User: UserResolvers = {
  isConfirmed: ({ user }) => {
    console.log("Called when user is User: " , { user });
    return user.confirmedAt !== null,
  }
}

export const Admin: AdminResolvers = {
  isConfirmed: ({ user }) => {
    console.log("Called when user is Admin: " , { user });
    return user.confirmedAt !== null,
  }
}

In this case, I've never seen the UserInterface.id to be called. That said, I could be missing something, so if you could share an example where it is called to help me understand the use case.


Last but not least, I'm wondering how this is related to federation

You are right, it is not 🙂. This major release includes a mix of type changes for both normal and Federation use cases, and this change is applicable to both.

@clemens
Copy link

clemens commented Sep 9, 2025

With the code as you've shown it, I agree: Resolvers higher up in the chain don't get called, if a lower resolver already resolves a field. But that wasn't my point: My point was that if a higher resolver defines it and a lower resolver doesn't, then the method of the higher resolver gets called. But this doesn't seem to be possible anymore with the changes in this PR.


I'll try and replicate our code as best as I can while including as little code as possible to not overwhelm you with our business logic… ;-)

We have the following GraphQL types:

"""
Interface to implement the base user type.
"""
interface UserInterface {
  id: ID!
  email: Email!
  """
  etc.
  """
}

"""
This interface carries all fields for a user that has verified their email and access to the app.
"""
interface VerifiedUserInterface implements UserInterface {
  id: ID!
  email: Email!
  """
  etc.
  """
}

"""
This interface carries all fields that are required for a user who has at least one policy.
"""
interface UserWithPolicyInterface implements UserInterface & VerifiedUserInterface {
  id: ID!
  email: Email!
  """
  etc.
  """
}

"""
The user has at least one confirmed policy and no unconfirmed policies.
"""
type User_WithPolicy implements UserInterface & VerifiedUserInterface & UserWithPolicyInterface {
  id: ID!
}

Note: The interfaces have further fields, the type has no further fields except the ID and the ones it gets from the interfaces.

Then, there are these resolvers:

import type { UserInterfaceResolvers } from "./types"

export const UserInterface: UserInterfaceResolvers = {
  __resolveType: ({ __typename }) => __typename!,
  id: ({ user }) => user.id,
  email: ({ user }) => {
    console.log("user.email in UserInterface", user.email)
    return user.email
  },
}
import type { VerifiedUserInterfaceResolvers } from "./types"

export const VerifiedUserInterface: VerifiedUserInterfaceResolvers = {
  __resolveType: ({ __typename }) => __typename,
  email: ({ user }) => {
    console.log("user.email in VerifiedUserInterface", user.email)
    return user.email
  },
}
import type { UserWithPolicyInterfaceResolvers } from "./types"

export const UserWithPolicyInterface: UserWithPolicyInterfaceResolvers = {
  __resolveType: ({ __typename }) => __typename,
}
import type { User_WithPolicyResolvers } from "./types"

export const User_WithPolicy: User_WithPolicyResolvers = {
  __resolveType: ({ __typename }) => __typename,
}

In a Jest test, I'm then executing a query like this:

query GetPayments {
  meV2 {
    __typename
    ... on UserWithPolicyInterface {
      email
    }
  }
}

Which logs user.email in VerifiedUserInterface [email protected].

When I add/remove the email method up and down the resolver chain (UserInterface > VerifiedUserInterface > UserWithPolicyInterface > User_WithPolicy), I get the expected results:

  • Adding it to User_WithPolicy always gives me that method's log output and no other (no matter if the others have the method or not)
  • Adding it to UserWithPolicyInterface gives me that method's log output, unless I also add it to User_WithPolicy
  • Adding it to VerifiedUserInterface gives me that method's log output, unless I also add it to User_WithPolicy and/or UserWithPolicyInterface
  • Adding it to UserInterface gives me that method's log output, unless I add it anywhere else

Does that help with illustrating the problem? Happy to dive deeper or provide additional info.

@eddeee888
Copy link
Collaborator Author

eddeee888 commented Sep 9, 2025

Hi @clemens ,

I'm unable to reprod using the provided materials 😞 . Maybe I'm missing the Query.meV2 implementation and/or the return type in its schema?

Could you help create an issue with minimal reprod using GitHub, Stackblitz or CodeSandbox please?

This will help me track and get on top of this 🙏

@clemens
Copy link

clemens commented Sep 9, 2025

I need to apologize: While trying to reproduce this in a Code Sandbox, I realized that we had a script in place that I wasn't aware of that does the magic. We're basically parsing all of our type definitions and then using our own little codegen to create extensions for every type that implements interfaces. In other words: For the example above, we'd actually codegen files like this:

extend type User_WithPolicy implements UserInterface & VerifiedUserInterface & UserWithPolicyInterface {
  email: Email!
  """
  etc.
  """
}

That then gets loaded together as part of the codegen where we're using your CLI to generate the final outputs. So your CLI would e.g. get this:

type User_WithPolicy implements UserInterface & VerifiedUserInterface & UserWithPolicyInterface {
  id: ID!
}

"""
etc.
"""

extend type User_WithPolicy implements UserInterface & VerifiedUserInterface & UserWithPolicyInterface {
  email: Email!
  """
  etc.
  """
}

Long story short: It's probably not an issue with your changes and we might have to adapt that script to account for the changes you made. I'll give that a try and report back to you in case it turns out to be an issue after all. (And if it is, I'd try to create a proper setup to reproduce.)

In the meantime, thanks for caring! <3

@eddeee888
Copy link
Collaborator Author

Thank you for collaborating with me on this @clemens!

I'm really appreciate you getting back and explaining your use case, to make sure we don't miss anything! 🙏
Please feel free to reach out anytime!

@clemens
Copy link

clemens commented Sep 22, 2025

@eddeee888 So I think I've run into a problem after all and I've managed to reproduce it in this CodeSandbox: https://codesandbox.io/p/devbox/objective-field-ssvjcd?workspaceId=ws_PEDGHUswTsoCvZUnDz9SSL

The situation occurs when you have an interface and a type that implements this interface:

interface UserInterface {
    id: ID!
    username: String!
    email: String!
}

type ConfirmedUser implements UserInterface {
    id: ID!
    username: String!
    email: String!
    confirmedAt: String!
}

When I'm then using the typescript and typescript-resolvers plugins, I'm getting different types for the resolver type of the interface:

// old version (types-old.ts):
export type UserInterfaceR[esolvers<ContextType = any, ParentType extends ResolversParentTypes['UserInterface'] = ResolversParentTypes['UserInterface']> = {
  __resolveType: TypeResolveFn<'ConfirmedUser', ParentType, ContextType>;
  email?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
  username?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
};

// new version (types-new.ts):
export type UserInterfaceResolvers<ContextType = any, ParentType extends ResolversParentTypes['UserInterface'] = ResolversParentTypes['UserInterface']> = {
  __resolveType: TypeResolveFn<'ConfirmedUser', ParentType, ContextType>;
};

Note the absence of the 3 interface fields (id, email, username) in the resolver, even though they're clearly defined in both the interface as well as the type that implements the interface.

The difference here seems to stem from onlyResolveTypeForInterfaces – because when I'm setting this to true in the old version, I'm getting the same result – see types-old-onlyResolveTypeForInterfaces.ts.

For for the new version (as well as the old version with onlyResolveTypeForInterfaces set to true), when I'm then trying to work with these resolvers like so:

export const UserInterface: UserInterfaceResolvers = {
  __resolveType: ({ __typename }) => __typename!,
  id: ({ user }) => user.id,
  // …
}

I'm getting errors like Object literal may only specify known properties, and 'id' does not exist in type 'UserInterfaceResolvers'.ts(2353). (And also Binding element 'user' implicitly has an 'any' type.ts(7031), but I'm fairly certain this is just a consequence of the former.)


I still don't fully understand the rationale behind this change. From your comment in #10221 and the referenced issue (#5648), am I correct to understand that this code is indeed never called? And thus the types in the old version were "wrong" in the sense that they required us to write what's effectively dead code?

Note that we're indeed using makeExecutableSchema with inheritResolversFromInterfaces set to true – so we're using the exact use case that's described in the issue. And while yes, the inheritResolversFromInterfaces option is false by default, I'm wondering if it's actually a "wrong" approach, since I'd say it's a reasonable way of code sharing (especially since the interfaces are already not "pure" and the __resolveType function is indeed shared with classes that implement the interface). And if it's considered a wrong approach, shouldn't the consequence be that the inheritResolversFromInterfaces also be removed from the respective package, because with the current situation, we effectively have 2 packages that are fundamentally not compatible anymore?

@eddeee888
Copy link
Collaborator Author

I still don't fully understand the rationale behind this change. From your comment in #10221 and the referenced issue (#5648), am I correct to understand that this code is indeed never called? And thus the types in the old version were "wrong" in the sense that they required us to write what's effectively dead code?

Yes, this is correct, purely from how GraphQL resolver works.

And if it's considered a wrong approach, shouldn't the consequence be that the inheritResolversFromInterfaces also be removed from the respective package, because with the current situation, we effectively have 2 packages that are fundamentally not compatible anymore?

This is interesting, I didn't know that makeExecutableSchema has inheritResolversFromInterfaces. I will bring this back to the team to see what we should do here.

@eddeee888
Copy link
Collaborator Author

Hi @clemens @mzl-md,

A new option was added addInterfaceFieldResolverTypes to help with this use case.
Note that this needs to be explicitly turned to true (unlike onlyResolveTypeForInterfaces) because it is not the default GraphQL behaviour.

I've updated the doc to clarify this behaviour and its relationship with makeExecutableSchema's inheritResolversFromInterfaces. Thank you for collaborating with me patiently! 🙏

@clemens
Copy link

clemens commented Oct 15, 2025

I can confirm that everything seems to work in our app by adding addInterfaceFieldResolverTypes in the necessary places!

Thanks for getting this fixed and thanks for the quick turnaround!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants