Graphql-code-generator: Typescript helper for drilling into nested types?

Created on 25 Oct 2019  路  9Comments  路  Source: dotansimha/graphql-code-generator

This is part question, part feature request.

I'm curious if anyone has uncovered a useful pattern or helper library for drilling into deeply nested, complex GraphQL response types, especially those that have unions along the path?

I'm curious @dotansimha if you've thought about adding some helper types to the lib to help drill down to specific parts of the graph?

Motivation

I have a lot of queries like this:

query SomeQuery($id: ID!) {
  node(id: $id) {
    id
    ... on Workspace {
      name
      savedQueries {
        edges {
          node {
            id
            name
            views {
              id
              name
            }
          }
        }
      }
    }
  }
}

This looks pretty simple, but there are some tricky paths:

  • node is an interface of many possible types
  • savedQueries and edges are arrays
  • views is an array of interface types

The generated response type looks like this (I've omitted some of the graph for brevity):

export type SomeQuery = { __typename?: "Query" } & {
  node: Maybe<
    | ({ __typename?: "OrganizationMembership" } & Pick<
        OrganizationMembership,
        "id"
      >)
    | ({ __typename?: "User" } & Pick<User, "id">)
    | ({ __typename?: "WorkspaceMembership" } & Pick<WorkspaceMembership, "id">)
    | ({ __typename?: "Workspace" } & Pick<Workspace, "name" | "id"> & {
          savedQueries: { __typename?: "SavedQueriesConnection" } & {
            edges: Array<
              { __typename?: "SavedQueriesEdge" } & {
                node: { __typename?: "SavedQuery" } & Pick<
                  SavedQuery,
                  "id" | "name"
                > & {
                    views: Array<
                      | ({ __typename?: "ViewChartXY" } & Pick<
                          ViewChartXy,
                          "id" | "name"
                        >)
                      | ({ __typename?: "ViewTable" } & Pick<
                          ViewTable,
                          "id" | "name"
                        >)
                    >;
                  };
              }
            >;
          };
        })
    | ({ __typename?: "Organization" } & Pick<Organization, "id">)
    | ({ __typename?: "OrganizationInvitation" } & Pick<
        OrganizationInvitation,
        "id"
      >)
    | ({ __typename?: "OrganizationRole" } & Pick<OrganizationRole, "id">)
    | ({ __typename?: "WorkspaceRole" } & Pick<WorkspaceRole, "id">)
    | ({ __typename?: "Dashboard" } & Pick<Dashboard, "id">)
    | ({ __typename?: "PublicAccess" } & Pick<PublicAccess, "id">)
    | ({ __typename?: "DashboardView" } & Pick<DashboardView, "id">)
    | ({ __typename?: "SavedQuery" } & Pick<SavedQuery, "id">)
  >;
};

What I'd like to be able to do is dynamically type any arbitrary section of SomeQuery.

Use case

The primary use case is to type React child components that accept data from a parent, which is typically a subset of the full response.

Current approach

For now, we're writing quite convoluted types that wind up looking like this (not related to the above query, but the general approach):

// lots of NonNullables! Unpacked converts T[] -> T 
type Node = NonNullable<Unpacked<NonNullable<SomeOtherQuery["node"]>["edges"]>>["nestedField"]["node"]["id"]

This is fine for basic queries, but for any where unions/interfaces form part of the response, there's additional inference and unpacking to go down certain paths.

I've looked at ts-toolbelt as an option to drill into types, which looks promising, but haven't figured out a general approach yet.

Ideal helper

Possibly with the help of recursive types in TS 3.7, I'm wondering if there's a way we might end up with a generic Query helper which can be used to probe arbitrary levels.

Something like the following would be awesome:

// Query being a type helper, and not a runtime function
type Node = Query<SomeQuery, ["node", "edges", "nestedField", "node"]>

Any child React component could then just have node: Node as a prop, and the shape would match a specific query exactly, instead of having to either a) take a full _SomeQuery_ or b) use the 'raw' type of the inner key, which is then unsafe because the field selection doesn't match this specific query.


Are there any plans to add helpers to the lib? Or perhaps any useful third-party libs that can get us close to the Query helper?

Would really appreciate any recommendations and love to hear how others are solving this.

Thanks!

All 9 comments

A "workaround" would be using Fragments for defining the Data a single component should receive. You can then use the generated Fragment type as a prop/props. I would categorize this as the relay way.

馃憤 Thanks @n1ru4l. Fragments definitely have a place, but I think defining them for every query can add unnecessary noise/overhead. A general purpose type that can focus on a specific part of the response keeps the query semantics cleaner for ad hoc queries, IMO.

I wound using ts-toolbelt to provide a 'lens' into a query. A modified version will soon be available in the lib as Object.PathUp for anyone wanting a more generalised approach.

I'll close this for now, since this addresses my specific use-case

Thanks @leebenson !
Actually ts-toolkit looks good, maybe we can create a small extension plugin that generates those intermediate types using that. I'll check it soon :)

Excellent!

As a starting point, https://github.com/pirix-gh/ts-toolbelt/issues/64#issuecomment-546610935 is what I wound up going with (with some minor changes.)

I also created a Typename<TQuery, TTypename> helper for discriminating a union/interface based on a specific __typename:

// Discriminate on __typename
type Typename<T, K> = T extends { __typename?: K }
  ? import("ts-toolbelt").Union.Select<
      import("ts-toolbelt").Object.Required<NonNullable<T>, "__typename">,
      { __typename: K }
    >
  : never;

That could probably be cleaned up a little, but it works well for my use-case.

Nice! Actually we have some code that does discrimination for TS types under typescript-compatibility, but I guess using ts-toolkit is better solution :)
BTW, typescript-compatibility might help you as well, it generates the intermediate types, but the purpose of it is to allow migration from older versions of the codegen

I am really quite disappointed with the 1.0 release of this project. It removed a lot of what made this tool better than the rest. You could separate client and server types, have namespaces that made the Query response types easy to get at. typescript-compatibility helps, sure, but it sounds temporary and not a long term solution. typescript-compatibility is also riddled with bugs around fragments. We previously used the types within the namespace to pass around as props, making all these a fragment is not really a great solution as we don't intend for these fragments to be re-used. It also adds request overhead (sure, its minimal), and the fragments to be defined away out of the context of the property they are spread in.

Sure you should seperate client-side and server-side code generation and we already have separated packages for those two purposes. typescript-compatibility is just there to help you in migration process not for the long term usage. If you catch bugs on it, you are free to open a new issue and submit a PR that fixes the issue (this would make us happier). I don't agree with you about using fragments because fragments improve legibility and reusability.
We stopped using namespaces because of several reasons such as incompability with CRA, Babel-Typescript and etc. Also it's been deprecated in TypeScript.

With the new typings being generated and recommendations of using fragments, and when the majority of your responses are passed around as props, then it requires you to turn everything into a fragment. The other issue with fragments is it can re-introduce overfetching, as any time you need access to a new property, you add it to the fragment, where as not all areas using the fragment need access to this. In cases where you aren't leveraging apollo cache, this causes overfetching.

Sure, you could add the one field to the query along with the fragment, but now you are back to issues with not being able to use the fragment for a complete typing to pass around.

Also, I cannot find anything regarding namespaces being deprecated in TypeScript.

According to the following, it is not deprecated.

https://github.com/microsoft/TypeScript/issues/30994

@Jonatthu I'd refer you to my comment upthread. Again: We've never removed any syntax from the language since 1.0 and don't intend to do so in the future.

For anyone reading this thread: Please don't trust internet randos about TypeScript's feature plans! Our roadmap is public anyone saying crazy stuff like that should be able to point to a roadmap entry for it.

Regarding Babel, they re-introduced namespace support.
https://github.com/babel/babel/pull/9785

Excellent!

As a starting point, millsp/ts-toolbelt#64 (comment) is what I wound up going with (with some minor changes.)

I also created a Typename<TQuery, TTypename> helper for discriminating a union/interface based on a specific __typename:

// Discriminate on __typename
type Typename<T, K> = T extends { __typename?: K }
  ? import("ts-toolbelt").Union.Select<
      import("ts-toolbelt").Object.Required<NonNullable<T>, "__typename">,
      { __typename: K }
    >
  : never;

That could probably be cleaned up a little, but it works well for my use-case.

Have you found a general solution for when a response has nested properties that are unions you'd like to narrow from the top level? i.e. You get a query response where the root entity can be of several __typenames, and its nested properties can also be of several __typenames, and you'd like to narrow everything before passing the root entity down to a component?

Would love to hear how far you got with this approach. Discriminating at a single level is straightforward, but it'd be excellent to narrow an entire object down rather than needing to check at each union as you access it in your application code, if that makes sense.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

NickClark picture NickClark  路  3Comments

edorivai picture edorivai  路  3Comments

SimenB picture SimenB  路  3Comments

jagregory picture jagregory  路  3Comments

iamdanthedev picture iamdanthedev  路  3Comments