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
onMutationUpdaterFunctions
with nested result types. This is happening on mutationupdate
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.
onCache.ModifyOptions Modifier
functions withReference[]
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!
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.