apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.

Home Page:https://apollographql.com/client

Repository from Github https://github.comapollographql/apollo-clientRepository from Github https://github.comapollographql/apollo-client

Typing Changes in Apollo Client 3.12.6 Generating TS2589 & TS7006 Errors

KeithGillette opened this issue · comments

Issue Description

Upgrading from Apollo Client 3.12.5 to 3.12.6 in an Apollo-Angular 8.0.0 / Angular 19 project generates two sets of TypeScript errors when building with tsc under TypeScript 5.7.3 (masked by a FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory error unless tsconfig.json has compilerOptions: { skipLibCheck: true } set):

  • error TS2589: Type instantiation is excessively deep and possibly infinite on MutationUpdaterFunctions with nested result types. This is happening on mutation update functions with several types that do have circular references but worked fine in Apollo Client 3.12.5 and previous versions.

  • error TS7006: Parameter '' implicitly has an 'any' type. on Cache.ModifyOptions Modifier functions with Reference[] arguments.

Both errors are resolved by reverting changes to Unmasked .

Link to Reproduction

Reproduction Steps

I have yet to reproduce the issue with a simple sample project, as it appears to only surface with multiple deeply nested types.

@apollo/client version

3.12.6

Hi @KeithGillette!

Thank you for the report, but we really need a reproduction for this or we won't be able to do anything - we already squashed some related bugs and we really need to see this in person.

It's very possible that there can be another "early exit" condition we can add in ContainsFragmentsRefs - but without seeing the type causing the problem we don't know which cases exactly to handle.

Thanks for your response, @phryneas. My attempts to create a simple reproduction failed but my Apollo Client Issue #12313 repository reproduces the issues with the relevant deeply nested and circular referenced types ripped directly from the project in which I'm experiencing the reported errors. Apologies for the complexity of the example.

This project will build and run correctly with @apollo/client@3.12.5 but not newer releases.

To reproduce the errors:

  npm install @apollo/client@latest
  tsc --noEmit --skipLibCheck
  npm run start

Hi Keith, that repository is not public - could you please make it public so I can take a look?

Oops—done!

This is irritating - either both of these should fail or none of them, but only the second one does.

We'll need to investigate this further, so far I unfortunately don't have an obvious solution here.

Image

Thank you for looking into this, @phryneas. Please let me know if there's anything further I can do to help isolate the issues caused by the typing changes to Unmasked in 3.12.6.

Hi, @phryneas — I see that Apollo Client 4 has entered alpha. Is a fix for this issue planned for version 4? Please let me know if there's more I can do to help isolate the errors introduced by the typing changes to Unmasked in 3.12.6.

Hey @KeithGillette 👋

I've looked at this a little bit more in detail and it looks like the culprit is that ContainsFragmentRefs guard is the main cause of this issue. If I remove that from the Unmasked type, it seems to work ok.

That said, I'm curious if these types are typical of an Angular app (apologies, I'm pretty unfamiliar with anything past Angular 1, especially when it comes to GraphQL). I noticed that the response type is an infinitely recursive type and not something you'd actually be able to actually select in a GraphQL document.

As an example, I can traverse a variable of that type infinitely in this way:

declare const data: IAssignmentUpdateAssignmentTaskStatus_ResponseData

// You can repeat `account.organization.assignmentList[0].assignmentTaskList[0]` as much as you want
data.AssignmentUpdateAssignmentTaskStatus.assignmentTaskList[0].activityList[0].account.organization.assignmentList[0].assignmentTaskList[0].activityList[0].account.organization.assignmentList[0].assignmentTaskList

The ContainsFragmentRefs helper chokes on the IAssignmentTask.activityList because its properties eventually lead back to IAssignmentTask.

Interestingly, I see the mutation as such:

  mutation AssignmentUpdateAssignmentTaskTaskStatus(
    $accountId: ID!
    $assignmentId: ID!
    $assignmentTaskId: ID
    $status: AssignmentTaskStatus!
  ) {
    AssignmentUpdateAssignmentTaskStatus(
      accountId: $accountId
      assignmentId: $assignmentId
      assignmentTaskId: $assignmentTaskId
      status: $status
    ) @client {
      _id
      assignmentTaskStatus
    }
  }

This document doesn't even select assignmentTaskList in the selection set. I don't know if this is just because its a reproduction thats stripped down, but the type mismatch is interesting here. I point this out because even this type worked as it does in 3.12.5, you'd be able to access properties in data that aren't returned by the mutation.

Interestingly enough, the UnwrapFragmentRefs type works fine even with the recursive type. This isn't to say we shouldn't try to fix ContainsFragmentRefs, but I wanted to at least point this out so that we have a better understanding of what to expect.

Thanks so much for digging into this, @jerelmiller!

These nested types have nothing to do with Angular. They are simply a feature of several data models we have. The client and server code is fully recursive to handle potentially unlimited nesting, but for practical purposes and due to GraphQL limitations, we limit depth on most models to 4, though at one point some of them went down to 7 levels.

The mutation in the reproduction is stripped down. The actual mutation in our application does select the assignmentTaskList:

mutation AssignmentUpdateAssignmentTaskTaskStatus($accountId: ID!, $assignmentId: ID!, $assignmentTaskId: ID, $status: AssignmentTaskStatus!) {
	AssignmentUpdateAssignmentTaskStatus(accountId: $accountId, assignmentId: $assignmentId, assignmentTaskId: $assignmentTaskId, status: $status) {
		_id
		assignmentTaskStatus
		assignmentTaskList {
			_id
			assignmentTaskStatus
			assignmentTaskList {
				_id
				assignmentTaskStatus
				assignmentTaskList {
					_id
					assignmentTaskStatus
				}
			}
		}
	}
}

Do you see a path to fix ContainsFragmentRefs to handle self-referential types?

Please let me know if you need more from me. Thanks again!

Ok thanks for confirming!

due to GraphQL limitations, we limit depth on most models to 4, though at one point some of them went down to 7 levels.

I think this is kinda my point with the way the models are setup right now. Currently the models in the reproduction can recurse infinitely because there is no "leaf" so to speak, where the mutation document has a definitive end (eventually you'll end up with a type that just has _id and assignmentTaskStatus and is no longer self referencing).

If the data type were 1:1 with the mutation document, I think ContainsFragmentRefs would work as expected since it wouldn't be able to infinitely recurse back to the same model. I was just wondering if the models generated (at least I assume these are generated in some way) in this app are what you'd typically see in a GraphQL angular app or if this was specific to the app you're working on since those models aren't 1:1 with the document.

I'll be meeting with @phryneas this morning to see if we can find a solution to this regardless, but knowing that information can help inform what direction we take 🙂

The typings aren't automatically generated. (We started before much GraphQL codegen existed.) They are built using interfaces based on the underlying domain model TypeScript classes, which have full recursive code to handle arbitrary nesting, which works fine everywhere else. As domain models, they don't have anything to do with Angular. That just happens to be the framework we use. Hope that's helpful!

Hi @KeithGillette,

we just had a quite long call going over this issue, and we don't think this is something we will be able to support.

When designing this, we had the underlying assumption here that the Apollo Client apis would be called with the exact types that would actually be returned from the server, and with that in mind we have quite a bunch of recursive types in there.
TypeScript doesn't exactly allow us to "stop recursing" when we reach a circular type (we do try to detect that here, but that's already quite memory-expensive and not 100% foolproof), and in the case that you provide here, we just can't stop it.

So, yeah, what you are doing here is a use case we honestly never had in mind and that we cannot support.

Even if we found a way around it, all these circularities will put a lot of strain to calculate on tsc, and as a result our types in combination with your types will absolutely clog the performance of your IDE - no matter how much we optimize there.

So, as a result, I can't really offer a good solution - you could patch-package here as some kind of escape hatch, but it will be tedious going forward.

My suggestion would be to drop those interfaces that extend Partial<DomainModel> classes and replace those with codegen. Setting up codegen is very straightforward nowadays, and since TypeScript is duck-typed, it will be possible to pass those generated types into the constructors of your domain models without any further modification.

As a result, you will gain a lot of type safety - and IDE performance - from that step.

I know, it's work, but you can probably do a testflight with just the few types that are problematic and then maybe do more replacements over time, getting more type safety in other places, too.

Thank you for your attention to this issue and clear explanations, @jerelmiller & @phryneas. I'm obviously disappointed that we will need to do significant extra work to refactor our code to use new @apollo/client releases, but understand your decision, appreciate the enhancements to type-safety and other improvements you're making, and have a path forward in exploring GraphQL code gen.

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.