pylon icon indicating copy to clipboard operation
pylon copied to clipboard

Adding union support as an input

Open silverAndroid opened this issue 9 months ago • 6 comments

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;
}

silverAndroid avatar Feb 04 '25 12:02 silverAndroid

Hi, this is a bit tricky because GraphQL does not support input unions.

How would you expect the respective graphql query to look like?

schettn avatar Feb 04 '25 14:02 schettn

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:

  1. Exclude this fields form the schema and show a warning
  2. Fallback to a generic scalar (lacks validation)

Wdyt?

schettn avatar Feb 04 '25 15:02 schettn

Personally, I'd prefer 2. That way I can at least perform the validation myself. The proposal LGTM!

silverAndroid avatar Feb 05 '25 00:02 silverAndroid

I will work on this after #62. You can test the canary release then.

schettn avatar Feb 05 '25 10:02 schettn

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.

schettn avatar Feb 05 '25 21:02 schettn

Yeah that's fine, it's more of a nice to have

silverAndroid avatar Feb 08 '25 01:02 silverAndroid