-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Generate valid types when the same field is selected by both conditional and unconditional fragments #4915
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
base: main
Are you sure you want to change the base?
Conversation
Hi @vhfmag! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at [email protected]. Thanks! |
30d8235
to
7d65662
Compare
5268dfe
to
de024dd
Compare
@evanyeung has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
1 similar comment
@evanyeung has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
Hi @vhfmag, sorry this has taken a while to get merged! It turns out that you exposed another issue in our typegen where we allow multiple keys to be generated in a single object more generally. This turns out to be valid Flow code so hasn't been noticed so far and we'll have to fix it before merging. Unfortunately, we're making a release today and this won't make the cut. We'll prioritize this to be included in our next release in 6 weeks. |
Hi @evanyeung! No worries, thanks for the update and for looking into this |
@evanyeung merged this pull request in a3ddd58. |
@evanyeung any issues with this PR? we noticed it was reverted 🤔 |
Yeah, I had to revert it as we are seeing an unexpected change in one of the types generated in our codebase. I'm digging into it and hope to re-land this still when I have an understanding on why the type was changed. |
I found some time to look into this and have a repro of where the bug. Using the query AppQuery($showLastName: Boolean!) @raw_response_type {
...AppFragment
...AppConditionalFragment
}
fragment AppFragment on Query {
me {
actor {
firstName
}
}
}
fragment AppConditionalFragment on Query {
me @include(if: $showLastName) {
actor {
lastName
}
}
} incorrectly overwrites the Is this something you'd be able to look in to @vhfmag? |
of course! I'll let you know once I have something ready @evanyeung |
1a3c7e3
to
3fdb39d
Compare
@evanyeung sorry for the delay, but hopefully the latest commit fixes the issue — I've modified the |
hey @evanyeung! just wanted to make sure you saw the message above so that we can include this fix in the next release if everything is working correctly now 😬 |
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.
Thanks for the update! I think there is still a small bug in the generated types here
@@ -27,6 +40,18 @@ export type AppQuery$data = {| | |||
|}; | |||
export type AppQuery$rawResponse = {| | |||
+me: ?{| | |||
+actor: ?{| |
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.
I think this is technically the incorrect type. Since the actor
field is included non-conditionally in the AppFragment
fragment, this type should be unconditional. We really only want the subfields from AppConditionalFragment
to be optional. I think this is due to the actor
in AppConditionalFragment
getting passed the conditionality of the me
field? But the non-conditional from the AppFragment
should override it.
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.
I'll look into it!
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.
@evanyeung I think this is actually correct, because User.actor
is typed as Actor
(which is nullable). it would be incorrect if the key-prop pair was optional (actor?:
). you can also see this is in line with the behavior elsewhere by looking at the type of the actor
field in the fragments, for which the field isn't conditional but is still nullable

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.
@evanyeung gentle bump on this
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.
Ah, I think you're right. Sorry for so many delays on this. I should be able to take another look next week.
I was locally testing this and I think the issue came in Flow where we currently have another bug where it's actually possible to generate two fields on an object type with the same name. When trying to collapse those fields into one field, I had to edit the code you most recently added that recursively merges the field types. That may be where I was seeing the wrong types on the Flow side and I incorrectly attributed them to the types in the test here.
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.
ah, gotcha!
@evanyeung has imported this pull request. If you are a Meta employee, you can view this in D80381458. |
We just ran into the same issue Stackblitz Reproduction ![]() My Question is - shouldn't it rather be a union instead of a merged type? query AppQuery($showLastName: Boolean!) @raw_response_type {
...AppFragment
...AppConditionalFragment
}
fragment AppFragment on Query {
me {
actor {
firstName
}
}
}
fragment AppConditionalFragment on Query {
me @include(if: $showLastName) {
actor {
lastName
}
}
} I believe for this example actor should be |
@jantimon In your example, there are two shapes
So if it were to be a union, it should be That said, while a union might be a more accurate representation in some cases, it's also less convenient. For example, imagine this setup: query AppQuery($include: Boolean!) @raw_response_type {
...AppFragment
...AppConditionalFragment
}
fragment AppFragment on Query {
me {
actor {
firstName
}
}
}
fragment AppConditionalFragment on Query {
me @include(if: $include) {
actor {
lastName
age
}
}
} In this case, a union ( But that comes at the cost of complexity: if we have two conditional fragments, the union would have 4 variants (fragment 1 and 2 skipped; fragment 1 included and 2 skipped; fragment 2 included and 1 skipped; fragment 1 and 2 included); more generally, if we have N conditional fragments, the union would have 2^N variants — which can get out of hand quickly and lead to TS performance issues, cryptic error messages, etc. Additionally, it requires (IMHO) clunky code to look for the conditional fields: // with optional fields, it's just typical JS/TS code
const name = data.lastName ? `${data.firstName} ${data.lastName}` : data.firstName;
// with a union type, we can't look for `data.lastName` without narrowing the type first,
// because it doesn't exist in one of the variants
const name = "lastName" in data ? `${data.firstName} ${data.lastName}` : data.firstName; |
This fixes #4914 — check the original issue for details about the bug this tries to solve. Here's how the types generated differ before and after this change:
More specifically, if both conditional and unconditional selections are present for a given linked field, it emits a single non-optional prop, but handles fields from the conditional selection as if they were conditional themselves (AKA
...Fragment @include(if: $condition)
+fragment Fragment on Type { field }
behaves the same as...Fragment
+fragment Fragment on Type { field @include(if: $condition) }
.