Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to map a relay (graphql) connection to an edge's nodes using generics with Typescript

I have recently been experimenting with React, GraphQL, Apollo, and Relay conforming queries.

Right now I have a GraphQL backend that has a simple GraphQL schema with some Connection queries for returning lists of entities that can be paginated using Relay server specifications.

On the front end, I've found myself repeating myself after making said queries with code that usually looks something like the below:

type isDefined = <T>(value: T | null | undefined): value is T => value !== null && value !== undefined;

export const AppleListComponent: React.FC = () => {
  const { data } = useAppleQuery(); // FWIW, generating this with graphql-code-generator
  const apples = data && data.apples;
  const appleNodes = apples && apples.edges.map(apple => (
    apple ? apple.node : undefined
  )).filter(apple => isDefined(apple));

  return (
    <div>
      {appleNodes && appleNodes.map(apple => (
        <p key={apple.text}>{apple.text}</p>
      ))}
    </div>
  );
};

The song and dance of checking for presence are necessary due to the Relay spec mandating that edges and nodes be Nullable.

I find myself often repeating this transformation from a relay format to a simple array of nodes format. I decided to see if I could make a helper function that takes something that conforms to a connection interface and returns either undefined or a list of mapped nodes.

That attempt looked like this:

type Maybe<T> = T | null;
type isDefined = <T>(value: T | null | undefined): value is T => value !== null && value !== undefined;

type Connection<T> = Maybe<{ edges: Array<Maybe<{ node: Maybe<T> }>>}>;

export const connectionToNodes = <T>(connection: Connection<T>): Maybe<T[]> => (
  connection && connection.edges.map(edge => (
    edge ? edge.node : undefined
  )).filter(isDefined)
);

Which allows the previous component to become

export const AppleListComponent: React.FC = () => {
  const { data } = useAppleQuery();
  const apples = data && data.apples;
  const appleNodes = connectionToNodes(apples);

  return (
    <div>
      {appleNodes && appleNodes.map(apple => (
        <p key={apple.text}>{apple.text}</p>
      ))}
    </div>
  );
};

My problem here is that my type for Connection of course does not exactly match up with my type I'm passing in which has more properties that look like:

Maybe<(
  { __typename?: 'AppleConnection' }
  & { edges: Array<Maybe<(
    { __typename?: 'AppleEdge' }
    & { node: Maybe<{ __typename?: 'AppleType' }
      & { __typename?: 'QuestionType' }
        & Pick<QuestionType, 'text'>
    > }
  )>> }
)>

So I end up getting errors about the question being of any type, and errors that what I am passing to connectionToNodes does not match the specified type.

My instinct is that I need to say my type T is a subtype of something else - of course for application purposes I don't care whether it's an Apple or Pear connection/edge/node, but I'd like for it to be ultimately inferred - any advice on how to achieve this?

like image 784
mcabrams Avatar asked Oct 26 '25 01:10

mcabrams


1 Answers

Try this helper to extract a node from a connection

export type ExtractNode<T extends { edges: any }> = NonNullable<NonNullable<T['edges'][number]>['node']>;

Usage

type Post = ExtractNode<FeedList_query['posts']>
like image 84
Sibelius Seraphini Avatar answered Oct 28 '25 16:10

Sibelius Seraphini