[relay-compiler]: Union types - issue 2870#5186
[relay-compiler]: Union types - issue 2870#5186morrys wants to merge 20 commits intofacebook:mainfrom
Conversation
| // This will never be '%other', but we need some | ||
| // value in case none of the concrete values match. | ||
| +__typename: "%other", | ||
| +name?: ?string, |
There was a problem hiding this comment.
Isn't this wrong? client_interface is of type ClientInterface and thus might have a future value added to it.
There was a problem hiding this comment.
Could you clarify what specific case you have in mind? A concrete example or test would help. thanks
There was a problem hiding this comment.
(Current types are client types which undercuts this example, but if we assume this is a server type...)
The types currently imply that type name will always be the literal ClientType. However, this JS code will be loaded in someone's browser during a long-lived session. During that time the server may ship with a new version that has an additional type that implements this interface. At that point the server might start returning a typename that is no ClientType. That's the purpose of %other. To ensure the client always handles the case that the server might start returning a type that wasn't know about when the client code was written.
This is because GraphQL has well defined "breaking change" semantics and adding a new implementor of a type is not considered a breaking change and thus the client must be able to safely handle such a change.
There was a problem hiding this comment.
In this specific case, the issue seems to stem from how Relay RelayResolver is currently handled.
There is a transformer, generate_relay_resolvers_operations_for_nested_objects, which previously ensured that __typename was generated. This allowed the type to be correctly detected by the condition:
by_concrete_type.values().all(|selections| has_typename_selection(selections))Looking a bit deeper, it seems feasible to remove the logic responsible for generating __typename from this transformer and instead introduce a dedicated transformer that handles the new behavior.
I tried building a draft implementation of a transformer for this, and I was able to correctly produce %other for pop_star_game, whilepop_star_name is still being ignored. I’m not entirely sure whether %other is actually required for that case as well.
I’ll continue looking into this transformer. From testing a few other cases, it seems to handle unions correctly without changes to the existing code, so it could be a good fourth option.
- Support the old heuristic
- Add a feature flag which enables users to opt back into the old behavior
- Add a codemod that inserts a top-level __typename in all the places where we were previously emitting a discriminated union.
- Add Generate Typename Field Transform
| @@ -1,7 +1,8 @@ | |||
| ==================================== INPUT ==================================== | |||
| fragment Foo on Node { | |||
| __typename | |||
There was a problem hiding this comment.
See above about this being a breaking change.
| || by_concrete_type | ||
| .values() | ||
| .all(|selections| has_typename_selection(selections))) | ||
| return base_fields.values().any(TypeSelection::is_typename); |
There was a problem hiding this comment.
Previously
... on SomeConcreteType {
__typename
someField
}
... on SomeOtherConcreteType {
__typename
anotherField
}
Would emit a discriminated union. Now it won't. That's going to be a big breaking change for a lot of users. Is there a reason we have to lose that?
There was a problem hiding this comment.
this isn’t a blocking issue—more about keeping things clean and having clearer control over when the discriminated union is emitted, especially since this is a common field shared across types.
It is technically a breaking change, but since it only affects typing/compilation, fixes should be guided by type errors and shouldn’t impact runtime logic.
That said, if this is a concern, I can add the condition by_concrete_type.values().all(|selections| has_typename_selection(selections)), resulting in:
return base_fields.values().any(TypeSelection::is_typename)
|| by_concrete_type.values().all(|selections| has_typename_selection(selections));There was a problem hiding this comment.
I think your rational is fine, but practically speaking we'll need some smooth migration path. Some options:
- Support the old heuristic
- Add a feature flag which enables users to opt back into the old behavior
- Add a codemod that inserts a top-level __typename in all the places where we were previously emitting a discriminated union.
There was a problem hiding this comment.
@captbaritone see the comment on the client interface
…cept pop_star_name.
|
I added the This is consistent with the new This simplifies usage on the user side: it is no longer necessary to manually add This avoids a breaking change and does not require updating existing queries, nor introducing feature flags or codemods. In this commit you will notice changes in several generated and/or expected files. This is expected, as discriminated unions are now correctly generated even in cases where @captbaritone let me know what you think, and feel free to take another look when you have time 👍 |
|
@tyao1 has imported this pull request. If you are a Meta employee, you can view this in D100882850. |
| if parent_field.selections.len() != fragment_spreads.len() { | ||
| errors.push(Diagnostic::error( | ||
| ValidationMessage::UpdatableOnlyInlineFragments { | ||
| outer_type_plural: self.executable_definition_info.unwrap().type_plural, | ||
| }, | ||
| parent_field.definition.location, | ||
| )); | ||
| } |
There was a problem hiding this comment.
Why are these validations removed?
There was a problem hiding this comment.
Hi @tyao1,
right now the validation only allows fields with inline fragments, like this:
query TestQuery @updatable {
node(id: 4) {
... on Page {
name
}
... on Comment {
firstName
}
}
}however, with the new union logic, including __typename should also be valid:
query TestQuery @updatable {
node(id: 4) {
__typename
... on Page {
name
}
... on Comment {
firstName
}
}
}because of that, the current check is no longer correct, so I removed it.
Alternatively, we could relax it to also allow __typename fields, what do you think?
[TypeGen] Refactor discriminated union handling in
visit.tsSummary
This PR refactors how Relay TypeGen handles discriminated unions in
visit.tsissue: #2870. The main goals are:should_emit_discriminated_union.__typenameconsistently.Changes in
visit.ts1. Remove unused function
Removed:
should_emit_discriminated_unionno longer uses this function and no other code depends on it.2. Update should_emit_discriminated_union
Before:
After:
Notes:
First change is an optimization:
base_fields.is_empty()is equivalent to the previousall(...) && any(...)check.Second change is more impactful: removed
|| by_concrete_type.values().all(...). Now,__typenameis checked in the parent and treated as a common field.3. Update
get_discriminated_union_astTwo major changes:
%othercase, include all parent fields instead of only__typename. This ensures the "other" object has complete common data.4. Test Updates
relay-client-id: handled as a union.
relay-resolver-with-output-type-client-interface: no longer treated as a union.
updatable tests
typename-on-union-with-non-matching-aliases:
TypenameOutsideWithAbstractType
TypenameInside
TypenameWithCommonSelections
NEW TEST: TypenameWithNestedCommonSelections: demonstrates correct handling of nested common fields.
Input:
Output: