pylon
pylon copied to clipboard
Adding union support as an input
Is your feature request related to a problem? Please describe. Similar to #33 but currently Pylon only supports unions as a return type, so I have to rely on an external validation library to make sure the input is correct.
Describe alternatives you've considered My workaround currently is to use a schema validation library like Zod and make the (Pylon) input type be as flexible as possible.
Additional context Example:
interface HasEmail {
email: string;
phoneNumber?: undefined;
}
interface HasPhoneNumber {
phoneNumber: string;
email?: undefined;
}
interface HasEmailAndPhoneNumber {
email: string;
phoneNumber: string;
}
type Contact = HasEmailAndPhoneNumber | HasEmail | HasPhoneNumber ;
// Ideally wouldn't need this interface, and could use Contact as my input
interface ContactInput {
email?: string;
phoneNumber?: string;
}
Hi, this is a bit tricky because GraphQL does not support input unions.
How would you expect the respective graphql query to look like?
I general I think it would be possible to support this. I could calculate the union and mark all fields as optional (except shared ones).
Where it gets tricky is when same keys have different values. For example:
interface HasEmail {
email: string;
phoneNumber?: undefined;
}
interface HasPhoneNumber {
id: number;
phoneNumber: string;
email?: undefined;
}
interface HasEmailAndPhoneNumber {
id: number;
email: string;
phoneNumber: number; // Phone number is now a number
}
type Contact = HasEmailAndPhoneNumber | HasEmail | HasPhoneNumber;
Since graphql does not support input unions it is not possible to use phoneNumber: string | number.
So we have two options:
- Exclude this fields form the schema and show a warning
- Fallback to a generic scalar (lacks validation)
Wdyt?
Personally, I'd prefer 2. That way I can at least perform the validation myself. The proposal LGTM!
I will work on this after #62. You can test the canary release then.
Hi @silverAndroid, I have to think on this matter a bit longer. For now I have created a typescript utility type that basically does what I proposed:
interface HasEmail {
email: string;
phoneNumber?: undefined;
}
interface HasPhoneNumber {
id: number;
phoneNumber: string;
email?: undefined;
}
interface HasEmailAndPhoneNumber {
id: number;
email: string;
phoneNumber: string; // Phone number is now a number
}
type Contact = HasEmailAndPhoneNumber | HasEmail | HasPhoneNumber;
// 1. Extract all keys from every member of the union.
type AllKeys<T> = T extends any ? keyof T : never;
// 2. Helper: Remove undefined from a type.
type ExcludeUndefined<T> = T extends undefined ? never : T;
// 3. Helper: Detect if a type is a union (excluding the possibility of undefined).
type IsUnion<T, U = T> =
T extends any ? ([U] extends [T] ? false : true) : never;
// 4. Collapse the type: if (after removing undefined) it’s a union then yield any; otherwise yield the remaining type.
type MergeValue<T> = IsUnion<ExcludeUndefined<T>> extends true ? any : ExcludeUndefined<T>;
// 5. Merge the union: For each key from AllKeys<T>, get its type from each branch, and apply MergeValue.
// The property is marked optional because not every branch might have that key.
type MergeUnion<T> = {
[K in AllKeys<T>]?: MergeValue<
T extends any ? (K extends keyof T ? T[K] : never) : never
>;
};
// Apply to our union:
type ContactInput = MergeUnion<Contact>;
/**
* This is the final type:
*
* type ContactInput = {
* email?: string | undefined;
* phoneNumber?: string | undefined;
* id?: number | undefined;
* }
*/
When to fields have different types it returns any.
You can then use the ContactInput in your mutation as an argument.
Yeah that's fine, it's more of a nice to have