Allow @hasInverse on union types

Why are we not allowed to use inverse relationships on union types? E.g.

type Foo {
   m: [Bar!]! @hasInverse(field: foo)
}

union Bar = Blub | Blabla

type Blub {
   foo: Foo!
}

type Blabla {
   foo: Foo!
}

throws:
Type Foo; Field m: Field m is of type Bar, but @hasInverse directive only applies to fields with object types

This makes adding childs to Foo impossible (unless you create them together with the parent)

Or am I doing something wrong?

I understand that I can use interfaces instead, this however leads to issues during code-generation for my apollo client as the interface type will be translated to a “normal” type where you can create instances of (which shouldn’t be allowed) (see: https://github.com/dotansimha/graphql-code-generator/discussions/5125)

As per GraphQL spec:

GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no guaranteed fields between those types.

Means, one can’t assume anything about the fields in member types of a union. If there are common fields in two types, then you should be using interfaces in your schema design to represent that abstraction.

Just for counter-example, I can add one more type to the union Bar like this, which doesn’t have a field foo:

type Foo {
   m: [Bar!]!
}

union Bar = Blub | Blabla | MyType

type MyType {
   myField: String
}

type Blub {
   foo: Foo!
}

type Blabla {
   foo: Foo!
}

The schema is still valid GraphQL, but now you can’t really apply @hasInverse because there is no field named foo in type MyType.

So, @hasInverse doesn’t make semantic sense with unions.

1 Like

But @hasInverse has no relevance outside of dgraph anyway, so does it really matter if the schema would still be valid? Dgraph could just check if all union members implement the required field and throw an error if not.

Alternatively, we could be more specific and allow @hasInverse only on unions which members implement the same interface:

type Foo {
   m: [Bar!]! @hasInverse(field: foo, interface: Hello)
}

interface Hello {
   foo: Foo!
}

union Bar = Blub | Blabla

type Blub implements Hello {
   ...
}

type Blabla implements Hello {
   ...
}

It is not about the validity of the schema, but promoting a better schema design. Dgraph could surely check the one-off special case of all the member types in a union having a common field. But, that is the exact reason why GrpahQL has interfaces. That is what interfaces are supposed to do. That is what it has always been with GraphQL: a better schema design to begin with. We have had discussions about this particular issue internally previously as well, and we decided not to allow hasInverse with unions because of the same reasons.

The intention of Unions is to be able to represent particular instances of different types, not all the instances of them in general. @michaelcompton has explained this tiny bit of difference here brilliantly:

You should be using an interface in this case, like this:

type Foo {
   m: [Hello!]! @hasInverse(field: foo)
}

interface Hello {
   id: ID!
   foo: Foo!
}

type Blub implements Hello {
   ...
}

type Blabla implements Hello {
   ...
}

and not a union.

For this particular case, you can always update Foo to add more children.

I believe the code generator that you are using, although it uses a type to represent interface in typescript, it is up to you to use it. You can choose to not create its instances, and anyways, if the GraphQL server you are interacting with doesn’t allow creating interfaces, you will eventually get an error from there if you do a mutation to create an interface. I understand that it feels weird creating instances of an interface on the client-side, but if the code generator is using type to represent interface, then either there is a reason behind it or that is something that can be improved there.

[EDIT]: We could surely re-discuss it again, and see if we should allow hasInverse on Unions, but it doesn’t make a semantic sense, is what has been the reason behind not allowing it yet after all.

1 Like

Thank you for your time and explanation. I also think that the codegen must be responsible to generate the correct types but wanted to ask if this problem was discussed here before and you perfectly answered this.

Just for further explanation as I felt my issue wasn’t clear (don’t bother to read, it’s completely unrelated to dgraph and more an issue with the codegen I’m using):

The codegen I’m using takes this schema:

type Foo {
   m: [Bar!]! @hasInverse(field: foo)
}

interface Bar {
   foo: Foo!
}

type Blub implements Bar {
   ...
}

type Blabla implements Bar {
   ...
}

generates theses types:

export type Bar = {
  foo: Foo;
};

export type Blabla = Bar & {
  __typename?: 'Blaba';
  foo: Foo;
  ...
};

export type Blub = Bar & {
  __typename?: 'Blaba';
  foo: Foo;
  ...
};

export type Foo = {
  __typename?: 'Foo';
  m: Array<Bar>;
};

As you can see, filed m of Foo is of type Bar. So I have no way of differentiating between the actual types (or read their fields) when using these generated types. Which defeats the purpose of code generation in this case.

This is of course completely unrelated to dgraph and I opened an issue on the codegen repo, I just had the feeling to explain myself.

2 Likes