Skip to content

Typescript helper for drilling into nested types? #2832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
leebenson opened this issue Oct 25, 2019 · 11 comments
Closed

Typescript helper for drilling into nested types? #2832

leebenson opened this issue Oct 25, 2019 · 11 comments

Comments

@leebenson
Copy link

leebenson commented Oct 25, 2019

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!

@leebenson leebenson changed the title Typescript helpers for drilling into nested types? Typescript helper for drilling into nested types? Oct 25, 2019
@n1ru4l
Copy link
Collaborator

n1ru4l commented Oct 28, 2019

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.

@leebenson
Copy link
Author

👍 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

@dotansimha
Copy link
Owner

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 :)

@leebenson
Copy link
Author

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.

@dotansimha
Copy link
Owner

dotansimha commented Oct 29, 2019

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

@mikebm
Copy link

mikebm commented Dec 3, 2019

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.

@ardatan
Copy link
Collaborator

ardatan commented Dec 3, 2019

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.

@mikebm
Copy link

mikebm commented Dec 4, 2019

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.

microsoft/TypeScript#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.
babel/babel#9785

@martdavidson
Copy link

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.

@batamire
Copy link

what do you use in 2023? 👀

@TriangularCube
Copy link

Still no particularly good solutions in 2023.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants