Union return type is too simple
Using the newest release, example:
Only id should be present.
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)
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' }
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
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.
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!
What about interfaces do they share the same faith?
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.
Yes please open an issue with a small reproduction
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:
After:
/**
* 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`