Interfaces
Recommended background reading
Interfaces in RescriptRelay
In RescriptRelay, depending on how you ask for the fields, interfaces are either unwrapped to a variant or a record with type specific fields being optional.
Let's clarify this with an example. Imagine this GraphQL schema:
interface Book {
title: String!
author: Author!
}
type Textbook implements Book {
title: String!
author: Author!
courses: [String!]!
}
type ColoringBook implements Book {
title: String!
author: Author!
colors: [String!]!
}
Here we have an interface Book
, which has title
and author
fields and Textbook
and ColoringBook
types which implements the Book
interface. Both types have one specific field, Textbook
- courses
and ColoringBook
- colors
.
If all the fields we ask for are within inline fragments of the types which implement the interface, the result will be a variant:
/* UserBook.res */
module BookFragment = %relay(`
fragment UserBook_book on Book {
... on Textbook {
title
courses
}
... on ColoringBook {
title
colors
}
}
`)
@react.component
let make = (~book) => {
let book = BookFragment.use(book)
// `book` would roughly be:
// type fragment =
// | Textbook({ title: string, courses: array<string> })
// | ColoringBook({ title: string, colors: array<string> })
// | UnselectedUnionMember(string)
// ]
switch book {
| Textbook({title, courses}) => <Textbook title courses />
| ColoringBook({title, colors}) => <ColoringBook title colors />
| UnselectedUnionMember(typename) =>
("Unselected member type: " ++ typename)->React.string
}
}
You could notice that in fragment definition we asked for title
field twice, which is unnecessary since it is a common field coming from the interface. We could instead specify it only once, above inline fragment spreads, on the Book
itself. This will produce a different result, a record type with type specific fields as optional values:
module BookFragment = %relay(`
fragment MyComponent_book on Book {
title
... on Textbook {
courses
}
... on ColoringBook {
colors
}
}
`)
@react.component
let make = (~book) => {
let book = BookFragment.use(book)
// `book` would be:
// type fragment = {
// title: string,
// courses: option<array<string>>,
// colors: option<array<string>>
// }
switch (book.courses, book.colors) {
| (Some(courses), None) => <Textbook title=book.title courses />
| (None, Some(colors)) => <ColoringBook title=book.title colors />
| (Some(_courses), Some(_colors)) => "Error! Somehow both courses and colors were received."->React.string
| (None, None) => "Error! Neither courses nor colors were received."->React.string
}
}
This way, we used the benefits of interfaces, but ended up having a bit harder time using its result.
Using the @alias
directive together with a inline fragment spread, we can get the benefits of both of the approaches above:
module BookFragment = %relay(`
fragment MyComponent_book on Book {
title
... @alias(as: "byType") {
__typename # You need to select this as well to get a variant back
... on Textbook {
courses
}
... on ColoringBook {
colors
}
}
}
`)
@react.component
let make = (~book) => {
let book = BookFragment.use(book)
// This exists directly on the object
let title = book.title
// Can switch on `byType` to get the rest
switch book.byType {
| Textbook({courses}) => <Textbook title courses />
| ColoringBook({colors}) => <ColoringBook title colors />
| UnselectedUnionMember(typename) =>
("Unselected member type: " ++ typename)->React.string
}
}