-
Notifications
You must be signed in to change notification settings - Fork 69
Emit separate interfaces for each type and its selections #64
Comments
An even more compelling argument could be made for unions, as there’s no way (that I know of?) to get a reference to a single type in a union with just TS. |
We have run into the same issue at our (still sigh) legacy based solution. The issue is that there's really not a great way to name these types. I think the intent behind relay is to have a named fragment (not used directly within a container) and then See: https://facebook.github.io/relay/docs/en/graphql-in-relay.html#relaymask-boolean |
FYI we solve this, somewhat, in our old solution using directives inside the GraphQL fragment type, ie. fragment on MyType {
name
profile @generateType(name: "Profile") {
id
name
}
} This still does not solve the union issue though, and it is also a bit complicated when the given type is a list type. |
It is possible to reference elements of a union using |
@ds300 Ooh nice one, didn’t know that one exist yet! And it’s so trivial in retrospect: |
@kastermester Oh interesting thought! Yeah, that technically works. Although I do think that means we need to be careful that the processed AST from relay-compiler doesn’t actually get required at runtime and added to the JS bundle generated by e.g. webpack. An easy fix would be for the fragment to exist in a separate module that never actually gets required anywhere, but I don’t like how that breaks the co-location aspect 🤔 Currently, however, this is breaking on our emitted fragment reference type checking. Consider the following example: export const ShippingAndPaymentReviewFragmentContainer = createFragmentContainer(
ShippingAndPaymentReview,
graphql`
fragment ShippingAndPaymentReview_order on Order {
fulfillmentType
shippingName
shippingAddressLine1
shippingAddressLine2
shippingCity
shippingPostalCode
shippingRegion
lineItems {
edges {
node {
artwork {
shippingOrigin
}
}
}
}
creditCard {
brand
last_digits
expiration_year
expiration_month
}
}
`
) ...which gets split up into: graphql`
fragment ShippingAndPaymentReview_orderAddressDetails on Order {
shippingName
shippingAddressLine1
shippingAddressLine2
shippingCity
shippingPostalCode
shippingRegion
}
`
export const ShippingAndPaymentReviewFragmentContainer = createFragmentContainer(
ShippingAndPaymentReview,
graphql`
fragment ShippingAndPaymentReview_order on Order {
fulfillmentType
...ShippingAndPaymentReview_orderAddressDetails @relay(mask: false)
lineItems {
edges {
node {
artwork {
shippingOrigin
}
}
}
}
creditCard {
brand
last_digits
expiration_year
expiration_month
}
}
`
) In this case the emitted typings look like: export type ShippingAndPaymentReview_orderAddressDetails = {
// ...
readonly " $refType": ShippingAndPaymentReview_orderAddressDetails$ref;
};
export type ShippingAndPaymentReview_order = {
readonly fulfillmentType: OrderFulfillmentType | null;
readonly shippingName: string | null;
// ...
}; For the data to be assignable to the address details type, the parent should include |
@kastermester The custom directive is an interesting alternative and simplifies some of these things, just feels a bit less clean to mix such details into the fragment. Although OTOH So yeah, might be something to talk to the Relay team about and also check if the emitted Flow types do work in the |
Without having tried this, does the type checking work if you move the directive to the fragment itself (not when it is being spread) ie. graphql`fragment ShippingAndPaymentReview_orderAddressDetails on Order @relay(mask: false) {
shippingName
shippingAddressLine1
shippingAddressLine2
shippingCity
shippingPostalCode
shippingRegion
} It was my understanding that this should work (from reading the docs) but I have not yet tried this myself. |
Oh also - the fragment ref is, the way I understand it, not supposed to be included in the fragment refs. The idea is not to use the
Directives with |
One last thing: One thing that should work however is that if we have the following fragments: fragment MyComponent_nonMaskedFragment on SomeType @relay(mask: false) {
id
otherType {
...OtherComponent_prop
}
}
fragment MyComponent_prop on SomeType {
...MyComponent_nonMaskedFragment
id
}
fragment OtherComponent_prop on SomeOtherType {
id
} Obviously using this - we need to be able to render export type MyComponent_nonMaskedFragment = {
// Not sure if this will be here or not:
readonly " $refType": MyComponent_nonMaskedFragment$ref;
// ...
readonly id: string;
readonly otherType: {
readonly " $fragmentRefs": OtherComponent_prop$ref;
};
// ...
};
export type MyComponent_prop = {
readonly " $refType": MyComponent_Prop$ref;
// ...
readonly id: string;
readonly otherType: {
readonly " $fragmentRefs": OtherComponent_prop$ref;
}
// ...
}; Does this make sense? |
@alloy @sibelius could you please refresh the status of this issue? As you stated at the beginning, separate types for each interface is something more than nice to have. |
Hello! We have a strong use-case for this kinda thing, especially because our schema is mostly nullable types, and we have type RecircNode = NonNullable<
NonNullable<
NonNullable<
NonNullable<
NonNullable<VideoPlayerNextUpData_viewer['realmContent']>['recircForNode']
>['edges']
>[0]
>['node']
>;
type RecircSeriesMember = NonNullable<
NonNullable<NonNullable<RecircNode['members']>['edges']>[0]
>['node']; It would be lovely to be able to add something like |
@tomconroy I am with you, it would be nice to get the types split in parts (apollo's way). In the meantime, just a hacky trick for similar case than yours, create a fragment of the node, and then spread it in the position needed. In this way I get the typings for that fragment. e.g. // issues_node.ts
import {graphql} from 'react-relay';
// tslint:disable-next-line: no-unused-expression
graphql`
fragment issuesNode on Issue @relay(mask: false) {
id
title
repository {
name
}
viewerDidAuthor
state
}
`; ==> generates // issuesNode.graphql.ts
import { ConcreteFragment } from "relay-runtime";
export type IssueState = "CLOSED" | "OPEN" | "%future added value";
declare const _issuesNode$ref: unique symbol;
export type issuesNode$ref = typeof _issuesNode$ref;
export type issuesNode = {
readonly id: string;
readonly title: string;
readonly repository: {
readonly name: string;
};
readonly viewerDidAuthor: boolean;
readonly state: IssueState;
readonly " $refType": issuesNode$ref;
}; Now we are a way closer, excepts the extra /**
* Removes recursively all traces of `$fragmentRefs` and `$refType` from type.
* @see https://github.com/relay-tools/relay-compiler-language-typescript/issues/29#issuecomment-417267049
*/
type NoFragmentRefs<T> = T extends object
? {
[P in Exclude<keyof T, ' $fragmentRefs' | ' $refType'>]: NoFragmentRefs<
T[P]
>
}
: T; |
I actually have come around and think that using fragments to split these up is the way it should be done, as per @kastermester's original comments. The major problem I see with trying to add such a feature is naming. In a single fragment you could be selecting different fields of the same type at various levels in the graph, because of that you can't just name a selection set after the type, but you'd have to somehow make them unique. By using fragments you get to control at what level you actually want this separation and how to name things. As for the fragment reference, I'm not sure what the upstream Flow emission does, but it seems to me that if a fragment is defined with the @jstejada @josephsavona do you have any thoughts on this? |
This. I strongly agree with your conclusion for the exact reasons you listed. We highly recommend using fragments as a way to describe a unit of data that you want to access. |
@josephsavona Thanks 🙏 |
@josephsavona @alloy thank you for the feedback, this makes sense. I did go down this route, trying to use |
So I originally found this issue because I needed to send parts of the type to a function to do work with it (similar to above), however this was successfully solved using fragments like suggested (yay). However now I've run into the union problem - did anyone solve this properly? I'm rendering a form and showing certain fields based on what type of product is being edited, while most fields are shared between the types (showing shipping settings for a physical product, but file settings for a digital one). I'm not really sure how to solve this? I'd preferably like to use a type guard
however I don't know how I would derive types Here is my example query product(uuid: $uuid) {
__typename
name
summary
description
... on PhysicalProduct {
estimatedProcessingTime
packagingType
}
... on DigitalProduct {
files { [...] }
}
} Any thoughts? |
Hm, I suppose maybe I could type FullPhysicalProduct = Common & PhysicalProduct
type FullDigitalProduct = Common & DigitalProduct Maybe? |
Given the following root query in my QueryRenderer: product(uuid: $uuid) {
...EditProduct_product @relay(mask: false)
} I then get graphql`
fragment EditProduct_product on Product @relay(mask: false) {
...EditProduct_common @relay(mask: false)
... on MerchProduct {
...EditProduct_merch @relay(mask: false)
}
}
`;
graphql`
fragment EditProduct_merch on MerchProduct @relay(mask: false) {
__typename
estimatedProcessingTime
packagingType
}
`;
graphql`
fragment EditProduct_common on Product @relay(mask: false) {
__typename
name
summary
description
}
`;
export type NoRefs<T> = T extends object ? Omit<T, " $refType" | " $fragmentRefs"> : T;
export type MerchProduct = NoRefs<EditProduct_common> & NoRefs<EditProduct_merch>;
export const isMerchProduct = (product: NoRefs<EditProduct_product>): product is MerchProduct =>
product.__typename === "MerchProduct"; Here is what I ended up doing - in case others are looking for a solution |
Can I just interject (and probably a side question); what is the use-case for |
@maraisr The use-case is things like utility functions that are not executing in a React context and therefore don't have access to the context's environment. |
Agreed though - now that we have |
Yeah, seems fair to close this by now 👍 |
Little dirty, but works for my use case // https://github.com/relay-tools/relay-compiler-language-typescript/issues/64#issuecomment-511765083
type NoRefs<T> = T extends object ? Omit<T, " $refType" | " $fragmentRefs"> : T;
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-inference-in-conditional-types
// https://stackoverflow.com/questions/43537520/how-do-i-extract-a-type-from-an-array-in-typescript#comment94888844_52331580
type Unpacked<T> = T extends Array<infer U> ? U : T extends ReadonlyArray<infer U> ? U : T;
type ExtractRelayEdgeNode<T> = NoRefs<
NonNullable<NonNullable<Unpacked<NonNullable<NonNullable<T>["edges"]>>>["node"]>
>; and use it like type EdgeNode = ExtractRelayEdgeNode<SomeEdges_list>;
type EdgeNode = ExtractRelayEdgeNode<SomeEdges_list["search"]>; |
@olso I get
in typescript 3.8.3 |
@CSFlorin Just hit it as well, this seems to work for me with typescript 4.0.1 // https://github.com/relay-tools/relay-compiler-language-typescript/issues/64#issuecomment-511765083
export type NoRefs<T> = T extends Record<string, unknown>
? Omit<T, " $refType" | " $fragmentRefs">
: T;
export type ExtractRelayEdgeNode<
T extends { edges: ReadonlyArray<{ node: any | null }> | null } | null
> = NoRefs<NonNullable<NonNullable<NonNullable<NonNullable<T>["edges"]>[0]>["node"]>>; |
@olso solutions worked for me but I needed one extra null of the edge node // https://github.com/relay-tools/relay-compiler-language-typescript/issues/64#issuecomment-511765083
export type NoRefs<T> = T extends Record<string, unknown>
? Omit<T, ' $refType' | ' $fragmentRefs'>
: T;
export type ExtractRelayEdgeNode<
T extends { edges: ReadonlyArray<{ node: any | null } | null> | null } | null
> = NoRefs<
NonNullable<NonNullable<NonNullable<NonNullable<T>['edges']>[0]>['node']>
>; |
Often we need to have a reference to a type that’s being used somewhere deeper into a fragment’s query. For instance, consider this fragment’s typings:
To some functions we end up passing a single element of the
artists
array, so currently in our code we do the following:It would be easy to emit the following artefact instead:
The only situation I imagine where it gets more complex is around naming the types when you have multiple selections on the same type in a single fragment. For instance, let’s say we had the following fragment typing:
In this case we can’t just call the type
Artist
, because we have to differentiate between the artwork’s artist and the artists related to the artwork’s artist.Encoding the path into the name of the type (e.g.
ArtworkSidebarArtists_artwork_artists_related_artists_Artist
) would be counter-productive, as in that case you could just as well use plain TS code to get the type reference (e.g.ArtworkSidebarArtists_artwork["artists"][0]["related_artists"][0]
).Maybe this is enough of an edge-case that we simply don’t emit named typings for such situations at all, which could instead encourage people to refactor that nested part of the query into a container/fragment of its own?
The text was updated successfully, but these errors were encountered: