facebook / relay

Relay is a JavaScript framework for building data-driven React applications.
https://relay.dev
MIT License
18.4k stars 1.82k forks source link

Type-safe updaters: non-assignability of unions #4560

Open Malien opened 9 months ago

Malien commented 9 months ago

I seem to have encountered an issue with the new typesafe updaters. Here's the basic repro: https://github.com/Malien/typesafe-relay-updaters-repro

Obligatory "using typescript, not flow" mention

Assume we have a simple Todo app with todo groupings. Like this:

Let us have the following gql schema:

type Query {
    todos: [Todo!]!
}

interface Node {
    id: ID!
}

union Todo = TodoItem | TodoGroup

type TodoItem implements Node {
    id: ID!
    title: String!
    completed: Boolean!
}

type TodoGroup implements Node {
    id: ID!
    title: String!
    todos: [TodoItem!]!
}

type Mutation {
  createTodo(title: String!): TodoItem!
}
fragment TodoList_assignable_todo on Todo @assignable {
  __typename
}

given the query:

query TodoListQuery {
  todos {
    ...Todo_todo # Some component that renders the todo items/groups
    ...TodoList_assignable_todo
  }
}

and the mutation

mutation TodoListAddTodoMutation($title: String!) {
  createTodo(title: $title) {
    ...Todo_todo
    ...TodoList_assignable_todo
  }
}

the update of

function updater(store, response) {
  const newTodo = response?.createTodo;
  if (!newTodo) return;

  const { updatableData } = store.readUpdatableQuery<TodoListUpdateQuery>(
    graphql`
      query TodoListUpdateQuery @updatable {
        todos {
          ...TodoList_assignable_todo
        }
      }
    `,
    {},
  );

  updatableData.todos = [...todos, newTodo];
}

fails to typecheck, since the generated types from TodoListAddTodoMutation is:

export type TodoListAddTodoMutation$data = {
  readonly createTodo: {
    readonly __id: string;
    readonly __isTodoList_assignable_todo?: "TodoItem";
    readonly " $fragmentSpreads": FragmentRefs<"TodoList_assignable_todo" | "Todo_todo">;
  };
};

__isTodoList_assignable_todo is optional.

This seems to be related to the sections of the docs "where is guaranteed to implement an interface"/"where is not guaranteed to implement an interface", yet this seems like a missed case.

TodoItem (the return type of createTodo) is guaranteed to """implement""" union Todo. Yet this requires additional type-guard function (aka. validators). In the end the new typesafe updaters code is a lot more cumbersome to deal with, in comparison to the old unsafe variant. Not to mention all of the new concepts of assignability and updatability being thrown back at you.

Malien commented 9 months ago

I'm exploring possibilities of opening a PR. Would need some time to familiarise myself with the compiler. Luckily compilers and Rust are not foreign to me