genql icon indicating copy to clipboard operation
genql copied to clipboard

Union return type is too simple

Open jasonkuhrt opened this issue 2 years ago • 9 comments

Using the newest release, example:

CleanShot 2023-02-22 at 10 48 07@2x

Only id should be present.

jasonkuhrt avatar Feb 22 '23 15:02 jasonkuhrt

that's a limitation of union types, given that the query could return any of the types of the union, the selection will just be X | Y, where X and Y are types of the union

I could use the on_X to only return the selection of fields of that types but that is not currently implemented (and would not be useful in case you have more than one on_X)

remorses avatar Feb 22 '23 16:02 remorses

I'm not sure which union type you're referring to (TS or GQL).

Runtime

We know based on the above code that GQL runtime will not include anything other than __typename and, if the union is Project, then id.

Buildtime

From a TS perspective the type here should be:

type Result =
  | { __typename: 'Project', id: string }
  // whatever other members are in the union...
  | { __typename: 'Foo' }
  | { __typename: 'Bar' }
  | { __typename: 'Qux' }
  | { __typename: 'Yop' }
  

jasonkuhrt avatar Feb 22 '23 18:02 jasonkuhrt

This is maybe possible but it’s too complex, I would need to use the new TypeScript template literal types and recursive types

I will keep this issue open in case I find a solution easy enough to implement

remorses avatar Feb 22 '23 20:02 remorses

I love a good TS typing challenge and would enjoy taking a crack at this. If I find the time I'll do so and submit a PR if any promising results are had.

jasonkuhrt avatar Feb 22 '23 21:02 jasonkuhrt

Thank you for creating this project! It's an excellent idea. But I was disappointed to see that the return types are not correctly typed according to the query. Looking forward to seeing this implemented!

RaeesBhatti avatar Mar 05 '23 00:03 RaeesBhatti

What about interfaces do they share the same faith?

aarne avatar Jun 19 '23 08:06 aarne

There is another complication arising because of this: when the schema is recursive it becomes impossible to get a non-recursive type from a query on a union. This hit me when I try to return a genql type from a Remix loader.

I get TS errors of the form Type of property 'role' circularly references itself in mapped type 'SerializeObject<UndefinedToOptional<DerivedRoleUserEdge & { __isUnion?: true | undefined; }>>'.ts(2615). If I leave out the union in the query selection it works fine.

I can try to create a minimal example of this if needed.

estyrke avatar Jun 30 '23 09:06 estyrke

Yes please open an issue with a small reproduction

remorses avatar Jun 30 '23 09:06 remorses

We rely on union types heavily, but don't have time to write our own GraphQL client right now. So we're taking a hybrid approach of developing some of our own type logic on top of GenQL. Below you can see what we have so far and some before/after results. It is not done and does not handle many things yet.

Before:

CleanShot 2023-12-14 at 17 14 15@2x

After:

CleanShot 2023-12-14 at 17 14 32@2x

/**
 * The follow types work around limitations in the GenQL types with regards to union types.
 * @see https://github.com/remorses/genql/issues/108
 */

/**
 * We need a lookup of all interfaces in the schema.
 * GenQL does not generate this for us. We need to do that manually here.
 * 
 * TODO interfaces lookup needs to be generated by genql...
 */
type Interfaces = {
  Error: true
}

type ArgsKeyName = '__args'
// type UnionBrandName = '__isUnion'
type UnionBrand = { __isUnion?: true }
type Objekt = { __typename: string }
type Union = Objekt & UnionBrand
// type OmitUnionBrand<T> = Omit<T, UnionBrandName>
type SelectionRoot = SelectionObject
type Selection = SelectionScalar | SelectionObject
type SelectionScalar = boolean
type SelectionObject = { [k: string]: Selection }

namespace SelectionSet {
  export type GetFragmentTypeName<T> = T extends string ? StripOnUnionPrefix_<T> : never
  type StripOnUnionPrefix_<T extends string> = T extends `on_${infer ObjectName}` ? ObjectName : never

  export type OmitArgs<$O extends SelectionObject> = $O extends { __args: any } ? Omit<$O, ArgsKeyName> : $O

  export type PickNonFragmentSelections<SS extends SelectionObject> = {
    [F in keyof SS as F extends `on_${infer _}` ? never : F]: SS[F]
  }

  export type GetFragmentObjectTypeNames<O> = keyof {
    [K in keyof O as GetFragmentTypeName<K>]: true
  }

  export type PickScalarSelections<SS extends SelectionObject> = {
    [K in keyof SS as SS[K] extends true ? K : never]: SS[K]
  }

  export type PickInterfaceFragmentSelections<$O extends SelectionObject> = {
    [K in keyof $O as GetFragmentTypeName<K> extends keyof Interfaces ? K : never]: $O[K]
  }

  export type ForObjektPickSelection<O extends Objekt, SS extends SelectionObject> = {
    [K in keyof O as K extends keyof PickScalarSelections<OmitArgs<SS>> ? K : never]: O[K]
  }
}

// --------

type InferQuerySelection<$R extends SelectionRoot> = {
  [K in keyof $R]: K extends keyof Query
    ? InferSelection<Query[K], $R[K]>
    : 'ERROR: Given Query object selection set field is not actually a field in the Query object.'
}

// prettier-ignore
type InferSelection<S, $S extends Selection> =
  S extends Union   ? $S extends SelectionScalar
                      ? 'ERROR: SelectionObject for union expected, got SelectionScalar'
                      : SelectionSet.ForObjektPickSelection<S, SelectionSet.PickNonFragmentSelections<Exclude<$S, SelectionScalar>>> &
                        InferInterfaceFragmentSelections<S,SelectionSet.PickInterfaceFragmentSelections<Exclude<$S, SelectionScalar>>> &
                        InferUnionFragmentSelections<S, Exclude<$S, SelectionScalar>> :

  S extends Objekt  ? $S extends SelectionScalar
                      ? 'ERROR: scalar selection on an object type'
                      : SelectionSet.ForObjektPickSelection<S, Exclude<$S,SelectionScalar>>

                    : 'ERROR: unknown kind of schema part: ${}'

type InferInterfaceFragmentSelections<SO extends Objekt, $O extends SelectionObject> = UnionToIntersection<
  Values<{
    [K in keyof $O]: $O extends SelectionScalar
      ? 'ERROR: scalar selection where fragment object selection expected'
      : SelectionSet.ForObjektPickSelection<SO, Exclude<$O[K], SelectionScalar>>
  }>
>

type InferUnionFragmentSelections<
  SO extends Objekt,
  $O extends SelectionObject,
> = SO['__typename'] extends SelectionSet.GetFragmentObjectTypeNames<$O>
  ? InferUnionFragmentSelection<SO, $O, SO['__typename']>
  : {} // eslint-disable-line

type InferUnionFragmentSelection<
  SO extends Objekt,
  $O extends SelectionObject,
  TypeName extends string,
> = `on_${TypeName}` extends keyof $O
  ? $O[`on_${TypeName}`] extends SelectionScalar
    ? 'ERROR: SelectionObject for union fragment expected, got SelectionScalar'
    : SelectionSet.ForObjektPickSelection<SO, Exclude<$O[`on_${TypeName}`], SelectionScalar>>
  : `ERROR: fragment field on_${TypeName} not in selection set`

jasonkuhrt avatar Dec 14 '23 22:12 jasonkuhrt