pylon icon indicating copy to clipboard operation
pylon copied to clipboard

How to discriminate results in Unions with same properties shape ?

Open Alahel opened this issue 7 months ago • 2 comments

Hello there.

Thanks for this amazing project, which helps us delivering an easy-to-maintain GQL API at my company 🤟

I have a question regarding how Pylon discriminates results in Union scenarios (related to Union of multi-entities searches). We found a solution, so we are not blocked, but I would like to have your opinion on this 👓

Considering this sample of types:

interface Node {
  id: number;
}

interface Pagination<T> {
  total: number;
  nodes: T[];
}

interface Toto extends Node {
  name: string;
  propOfToto: string;
}

interface Tata extends Node {
  name: string;
  propOfTata: string;
}

interface PaginatedToto extends Pagination<Toto> {
  isToto: true;
}

interface PaginatedTata extends Pagination<Tata> {
  isTata: true;
}

type SearchResult = PaginatedToto | PaginatedTata;

and this graphql query with a set of basic data:

export const graphql = {
  Query: {
    searchResults: () => {
      return [
        {
          total: 2,
          isToto: true,
          nodes: [
            {
              id: 1,
              name: "Toto1",
              propOfToto: "propOfToto1",
            },
            {
              id: 2,
              name: "Toto2",
              propOfToto: "propOfToto2",
            },
          ],
        },
        {
          total: 3,
          isTata: true,
          nodes: [
            {
              id: 11,
              name: "Tata1",
              propOfTata: "propOfTata1",
            },
            {
              id: 12,
              name: "Tata2",
              propOfTata: "propOfTata2",
            },
            {
              id: 13,
              name: "Tata3",
              propOfTata: "propOfTata3",
            },
          ],
        },
      ] as SearchResult[];
    },
  },
};

we did not find a proper way to discrimate results in our fragments, as it seems that pylon infers the final type according to properties of types mentionned in the Union, but what if those types shares the same properties like "total" and "nodes" in this case ?

Image

Image

we ended with an injection of a specific property inside each type

Image

Image

So I was wondering if there is a better way to hint pylon about how to resolve some types ? something like

resolvers: { SearchResult: { __resolveType: function resolveType(node) {
    if (node && typeof node === "object") {
      if (node.klass === "PaginatedToto" && "total" in node && "nodes" in node) {
        return "PaginatedToto";
      }
      ;
      if (node.klass === "PaginatedTata" && "total" in node && "nodes" in node) {
        return "PaginatedTata";
      }
      ;
    }
  } }

Alahel avatar Jul 02 '25 16:07 Alahel

Hi! Thanks for the kind words — really glad to hear Pylon is helping you out!

You’re absolutely right about union discrimination: Pylon uses structural inference to resolve union types by inspecting the properties of each object at runtime.

When the types in a union share overlapping fields like total and nodes, Pylon can't disambiguate them unless each type has at least one uniquely identifying property. That’s why, in your example, flags like isToto: true and isTata: true are essential — they serve as clear discriminators that help the resolver select the correct type.

I tested this locally with your provided code, which generated the expected .pylon/resolvers.js:

export const resolvers = {
  SearchResult: {
    __resolveType: function resolveType(node) {
      if (node && typeof node === "object") {
        if ("isToto" in node && "total" in node && "nodes" in node) {
          return "PaginatedToto";
        }
        if ("isTata" in node && "total" in node && "nodes" in node) {
          return "PaginatedTata";
        }
      }
    },
  },
};

If the types don't have unique fields, Pylon will emit a warning like this:

 WARN  Warning: Union types "PaginatedTata" and "PaginatedToto" have the same fields: [nodes, total].               4:40:07 PM
Consider differentiating these types by adding unique fields or using different type names.
This may cause ambiguity in type resolution.

Another approach is to use a dedicated __typename or kind field in your data model:

interface PaginatedToto extends Pagination<Toto> {
  kind: "PaginatedToto";
}

interface PaginatedTata extends Pagination<Tata> {
  kind: "PaginatedTata";
}

This doesn’t currently work out-of-the-box, but since kind is a string literal type, I could add support for that in the resolver logic:

export const resolvers = {
  SearchResult: {
    __resolveType: function resolveType(node) {
      if (node && typeof node === "object") {
        if (node.kind === "PaginatedToto" && "total" in node && "nodes" in node) {
          return "PaginatedToto";
        }
        if (node.kind === "PaginatedTata" && "total" in node && "nodes" in node) {
          return "PaginatedTata";
        }
      }
    },
  },
};

That said, I’m still unsure whether this can be fully addressed without some form of explicit definition in the types — though in theory, __resolveType could attempt to match the object's structure against the entire GraphQL type definitions to infer the correct one. I'm just not sure yet what the performance implications of that would be. If there’s interest, I’d be happy to explore supporting this kind of resolution out of the box.

schettn avatar Jul 03 '25 15:07 schettn

Thanks for your answer on this, I finally used the discrimination property with something like

export class PaginatedCmsUsers {
	klassCmsUser = true

	total: () => Promise<number>
	nodes: () => Promise<CmsUser[]>

	constructor(total: () => Promise<number>, nodes: () => Promise<CmsUser[]>) {
		this.total = total
		this.nodes = nodes
	}
}

while pylon relies on the klassCmsUser prop in union scenarios.

Your suggestion on the string literal is really interesting, this could help a lot with generic functions

const kind: "PaginatedToto" | "PaginatedTata"

Alahel avatar Jul 03 '25 15:07 Alahel