Apollo-tooling: Proposal: Expose types for nested objects in Flow/Typescript targets

Created on 2 Jul 2017  Â·  26Comments  Â·  Source: apollographql/apollo-tooling

Right now, for the Flow and Typescript targets, we generate 1:1 types per operation and fragment.

I have a fork that generates nested types and we've been using this. I was wondering how useful this would be to other people and if we should pull this into master.

The type names that are generated can get very verbose, but having all the intermediate types allows for more flexibility around how the types are used.

cc @rricard I know we talked about this before. The code change is fairly minimal to get this to work.

For instance, for the following GraphQL:

query HeroQuery {
  hero(episode:NEWHOPE) {
    name
    friends {
      ... on Droid {
        primaryFunction
      }
      ... on Human {
        appearsIn
        homePlanet
        friends {
          ... on Droid {
            primaryFunction
          }

          ... on Human {
            homePlanet
          }
        }
      }
    }
  }
}

we generate a type like this.

/* @flow */
//  This file was automatically generated and should not be edited.

export type HeroQueryQuery = {|
  hero: ?( {
      __typename: "Human",
      // The name of the character
      name: string,
      // The friends of the character exposed as a connection with edges
      friendsConnection: {|
        __typename: string,
        // A list of the friends, as a convenience when edges are not needed.
        friends: ?Array< ( {
            __typename: "Human",
            // The name of the character
            name: string,
          } | {
            __typename: "Droid",
            // The name of the character
            name: string,
          }
        ) >,
      |},
    } | {
      __typename: "Droid",
      // The name of the character
      name: string,
      // The friends of the character exposed as a connection with edges
      friendsConnection: {|
        __typename: string,
        // A list of the friends, as a convenience when edges are not needed.
        friends: ?Array< ( {
            __typename: "Human",
            // The name of the character
            name: string,
          } | {
            __typename: "Droid",
            // The name of the character
            name: string,
          }
        ) >,
      |},
    }
  ),
|};

export type CharacterFragmentFragment = ( {
      __typename: "Human",
      // The name of the character
      name: string,
    } | {
      __typename: "Droid",
      // The name of the character
      name: string,
    }
  );

Generating named types for all the nested objects looks like this.

/* @flow */
//  This file was automatically generated and should not be edited.

/* GraphQL Operation: HeroQuery */

export type HeroQueryQuery = {|
  hero: ?(HeroQueryQuery_hero_Human | HeroQueryQuery_hero_Droid),
|};

export type HeroQueryQuery_hero_Droid = {|
  __typename: "Droid",
  // The name of the character
  name: string,
  // The friends of the character exposed as a connection with edges
  friendsConnection: HeroQueryQuery_hero_Droid_friendsConnection,
|};

export type HeroQueryQuery_hero_Droid_friendsConnection = {|
  __typename: string,
  // A list of the friends, as a convenience when edges are not needed.
  friends: ?Array< (HeroQueryQuery_hero_Droid_friendsConnection_friends_Human | HeroQueryQuery_hero_Droid_friendsConnection_friends_Droid) >,
|};

export type HeroQueryQuery_hero_Droid_friendsConnection_friends_Droid = {|
  __typename: "Droid",
  // The name of the character
  name: string,
|};

export type HeroQueryQuery_hero_Droid_friendsConnection_friends_Human = {|
  __typename: "Human",
  // The name of the character
  name: string,
|};

export type HeroQueryQuery_hero_Human = {|
  __typename: "Human",
  // The name of the character
  name: string,
  // The friends of the character exposed as a connection with edges
  friendsConnection: HeroQueryQuery_hero_Human_friendsConnection,
|};

export type HeroQueryQuery_hero_Human_friendsConnection = {|
  __typename: string,
  // A list of the friends, as a convenience when edges are not needed.
  friends: ?Array< (HeroQueryQuery_hero_Human_friendsConnection_friends_Human | HeroQueryQuery_hero_Human_friendsConnection_friends_Droid) >,
|};

export type HeroQueryQuery_hero_Human_friendsConnection_friends_Droid = {|
  __typename: "Droid",
  // The name of the character
  name: string,
|};

export type HeroQueryQuery_hero_Human_friendsConnection_friends_Human = {|
  __typename: "Human",
  // The name of the character
  name: string,
|};
/* GraphQL Fragment: CharacterFragment */

export type CharacterFragmentFragment = (CharacterFragmentFragment__Human | CharacterFragmentFragment__Droid);
export type CharacterFragmentFragment__Droid = {|
  __typename: "Droid",
  // The name of the character
  name: string,
|};

export type CharacterFragmentFragment__Human = {|
  __typename: "Human",
  // The name of the character
  name: string,
|};

Any thoughts?

Most helpful comment

:) Cool, good thing the code is mostly done since we've been using it in-house -- just gotta clean it up a bit. I'll open a PR on this soon!

All 26 comments

And how do you use the nested types in other parts of your code? Would be helpful to see what that looks like!

Yeah, sure thing. Here's a quick example (I left out pieces that aren't that important)

In a React app (using react-apollo), you might have a React Component file that looks like this.

import { graphql } from 'react-apollo';
import type {
  HeroQueryQuery_hero_Droid_friendsConnection_friends_Droid,
  HeroQueryQuery_hero_Droid_friendsConnection_friends_Human
} from 'generated/graphql/types';

class DroidFriend extends React.Component {
  props: HeroQueryQuery_hero_Droid_friendsConnection_friends_Droid
  render() {
    ...
  }
}

class HumanFriend extends React.Component {
  props: HeroQueryQuery_hero_Droid_friendsConnection_friends_Human
  render() {
    ...
  }
}

class HeroData extends React.Component {
   ...
   render() {
    const {
      name,
      friends
    } = this.props.data;

     return (
       <div>
          <div>Name: {name}</div>
          {
            friends
              .map(friend => {
                switch (friend.__typename) {
                  case 'Droid': return <DroidFriend {...friend} />
                  case 'Human': return <HumanFriend {...friend} />
                }
              })
          }
       </div>
     )
   }
}

export const withHeroData = graphql(gql`
  query HeroQuery {
    hero(episode:NEWHOPE) {
      name
      friends {
        ... on Droid {
          primaryFunction
        }
        ... on Human {
          appearsIn
          homePlanet
          friends {
            ... on Droid {
              primaryFunction
            }

            ... on Human {
              homePlanet
            }
          }
        }
      }
    }
  }
`);

export default withHeroData(Hero);

Notice how the generated nested types can be used to provide more safety around the usage of the HumanFriend and DroidFriend components.

@lewisf that's pretty cool to have I think. I did not think of that when implementing the TS/Flow generation but I can see how I would use it now!

I'll be happy to look at a PR for flow and port it to TS afterwards... I don't know if swift could benefit from it as well (cc @martijnwalraven) but I doubt it...

This is in fact very similar to what the Swift target already does, but with actual nested types. So it generates types like HeroQuery.Data.Hero.Friend.AsDroid.

@martijnwalraven This is pretty cool! Let's do it for Flow & TS then!

:) Cool, good thing the code is mostly done since we've been using it in-house -- just gotta clean it up a bit. I'll open a PR on this soon!

I'd love this!

Love it! Great idea.

@lewisf are you still working on this? This would be an awesome addition to apollo-codegen

@billychorus hey! sorry, I got pulled into a bunch of other stuff @ work and lost track of this task. I have some of the code done already (but only for the Flow type generation) and would love to merge in Flow and TS in at the same time. If you're interested in working on it, I'm happy to show you the code changes I had made for Flow -- otherwise I'll try to prioritize this task soon

@lewisf If you already have a flow PR, you should just create of it, I'll take the time to create the TS counterpart in an another PR, and we can merge both at the same time.

+1

@lewisf I think this would be a really useful feature. I'm wondering about the right approach for naming/placing the types though.

Not sure if I'm missing something, but the DroidFriend component could theoretically appear in multiple queries, so maybe it's worth detaching it from the query prefix?

I could imagine something like this:

import React from 'react';
import gql from 'graphql-tag';
import type {DroidFriend_droid} from 'types/graphql/DroidFriend';

type Props = {
  droid: DroidFriend_droid
};

const DroidFriend = (props: Props) => (
  <span>{this.props.droid.primaryFunction}</span>
);

DroidFriend.fragments = {
  droid: gql`
    fragment DroidFriend_droid on Droid {
      primaryFunction
    }
  `
};

export default DroidFriend;

The import would require unique component names across the app, but I think that's an acceptable requirement.

I changed the interface of the component a bit for this example. To avoid "fat queries" in container components and to colocate data requirements with the components using them I used a fragment for specifying the data the DroidFriend component needs via a prop.

I used the fragment naming as it's advised in Relay:

A naming convention of <FileName>_<propName> for fragments is advised.

It seems like this is also becoming popular with apollo.

This would allow multiple fragments being passed to multiple props.

I'm very new to flow, but I think we could also export a single type that can be used as an intersection:

import type DroidFriendData from 'types/graphql/DroidFriend';

type Props =
  & {
    someOtherProp: string
  }
  & DroidFriendData;

// Or if the component receives only GraphQL data even simpler:
type Props = DroidFriendData;

This approach would imply that we don't create a type for every single nested type, but rather for every fragment/query that is specified (i.e. one for every component needing GraphQL data).

What do you think?


Edit: Just read that Relay is actually doing basically the same thing:

import type {DictionaryComponent_word} from './__generated__/DictionaryComponent_word.graphql';

Another edit: Just found out that apollo-codegen already splits types into fragments 😅. Alright, I guess that solves my use case – sorry 😀

@amannn I'm currently in the process of rewriting the flow target to use a newer intermediate representation that @martijnwalraven has been working on and love what Relay is doing with where it places types. I'm going to take your feedback into account and see if I can get it into the rewrite?

This seems really useful. I want it in typescript so I can divide my render function into smaller functions that can declare their arguments using the the nested types.

What is the status on this?

@ecstasy2 this is actually available in the flow-modern target -- which includes other changes like co-locating type definitions so we can avoid ridiculously long type files

I'm going to close this since I'm focusing on the newer flow-modern target, which will eventually be the standard flow target. Please comment if you have concerns!

What about Typescript?

@clayne11

Coming shortly too - hoping to finish a typescript-modern target this weekend as well that includes the same change. Hopefully PR incoming tonight or tomorrow.

@clayne11 check out #304's snapshots! please leave any feedback you may have in #304's comments

Did this ever get implemented for TypeScript? The thread makes it sound like it was done a while ago, but I still see one large complex object with the nested types inside.

I switched one of my projects to graphql-code-generator which works this way for typescript (If you're looking for an alternative).

@matt-senseye Are you using the typescript-modern target? I believe that should generate separate types.

@martijnwalraven Thanks - the typescript-modern targer is a lot closer to what I was expecting! I had no idea it existed. One other thing, the --output argument doesn't seem to have any effect with typescript-modern as it puts it in a path near the source of the query (in a __generated__ folder). Wondering if that's configurable but not sure where to look. There doesn't seem to be any mention of it in the README, nor clues in apollo-codegen/src/typescript/. Do you know if the "-modern" target is documented?

@shadaj is working on cleaning up the repo and making the -modern targets the default, so things should be in a better shape soon! Could you open a separate issue about the --output argument so we can be sure this gets addressed?

Was this page helpful?
0 / 5 - 0 ratings