Skip to content

Overriding type of union works as declared type but not as generic type #61729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
SimonSimCity opened this issue May 19, 2025 · 1 comment
Open
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@SimonSimCity
Copy link

🔎 Search Terms

map, union, generics, property does not exist on type

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about Common "Bugs" That Aren't Bugs and Generics.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAZiEMC8MA8AoGMCiBhAUwBsiYCAPKAsAEwhgG8YoBPABwIC4YAiYggLbUoPANxkig4QEka3aACcAlmADmMAL4AaLDABihEmUrU6jZuy684REAHcx8W3dnyoytZp3Z8IUhSpaegZdbFYObj5JITARUVCYYGIiCG4ACnxkmAAffUMiAEoAbQBdeOxsNgBDGhoVVW4mSTgobjAAVwEAIwIFcWVVAAtWmA7u3s1yr10DP2NAsxCKiwjrZzEEm3tXGEV6qe0MAD400BT031Jc2cLSgpQjxl1QSFgIQSUAZXd24Ch2hQEGh4Pz0VBnCAAOgEVTYaVOfnuyCOCTOkPCBBQyFQUSksR4CWwAH4nssyTBIZSzt5yRUkiRUok-JD6SlobD4cAkSjaWTgOjLFicVsHDASUxKSytE57IywAQ7DAAIIKBRVFhoJginZ7TwaE73DQwbjAGm8gpmskaQnGplEGkFeK6QH-BRgXYfb4KX6uoEgtkwuEIwoPVHMjFC3j8GIiG3im3LSXUhN05KMtGsqGBzncxICjiR3ExnhixK2hhJ6Ui9OQ6vsuFwB7wAoaC0p63kk1+B3xDTxIA

💻 Code

If I try to modify a generic nested map-structure and add a property to an object deep in the hierarchy, the generic type cannot follow up and still thinks the object contains the old type. In the following examples, it's all about the type of semiStructuredCols, as I show in the map-functions following the definition.

Example 1:
In this example, I do not assign a specific type, but let the system figure out what I do. It's pretty good, but it seems to keep the parameter cells as in the original object and doesn't use Omit<> to remove it before adding it with the new type. You can see it fail a bit further down in line 29 of the example because of the wrong type.

const foo = <
  ECell extends { type: "element"; elementId: string },
  FCell extends { type: "flow"; flowId: string },
  ECol extends {
    type: "element";
    cells: (ECell | FCell)[];
    padding: { left: number; right: number };
  },
  FCol extends {
    type: "flow";
    flowId: string;
  },
>(cols: (ECol | FCol)[]) => {
  const semiStructuredCols = cols.map((col) =>
    col.type === "element"
      ? {
          ...col,
          cells: col.cells.map((c) =>
            c.type === "flow" ? { ...c, flows: new Array<{ flowId: string }>() } : c,
          ),
        }
      : col,
  );

  return semiStructuredCols.map((col) =>
    col.type === "element"
      ? {
          ...col,
          cells: col.cells.map((c) => c.type === "element" ? c : {...c, flows: c.flows.map(f => f)}),
        }
      : col,
  );
};

Example 2:
In this example, I try to manually set the type on the variable, which fails miserably - even earlier, but still on line 29 of the example.

const foo = <
  ECell extends { type: "element"; elementId: string },
  FCell extends { type: "flow"; flowId: string },
  ECol extends {
    type: "element";
    cells: (ECell | FCell)[];
    padding: { left: number; right: number };
  },
  FCol extends {
    type: "flow";
    flowId: string;
  },
>(cols: (ECol | FCol)[]) => {
  const semiStructuredCols: ((Omit<ECol, "cells"> & { cells: (ECell | (FCell & { flows: { flowId: string }[] } ))[] }) | FCol)[] = cols.map((col) =>
    col.type === "element"
      ? {
          ...col,
          cells: col.cells.map((c) =>
            c.type === "flow" ? { ...c, flows: new Array<{ flowId: string }>() } : c,
          ),
        }
      : col,
  );

  return semiStructuredCols.map((col) =>
    col.type === "element"
      ? {
          ...col,
          cells: col.cells.map((c) => c.type === "element" ? c : {...c, flows: c.flows.map(f => f)}),
        }
      : col,
  );
};

Example 3:
I gave up on generic types and tried an explicit type - this seems to work quite well.

type ECell = { type: "element"; elementId: string };
type FCell = { type: "flow"; flowId: string };
type ECol = {
  type: "element";
  cells: (ECell | FCell)[];
  padding: { left: number; right: number };
};
type FCol = {
  type: "flow";
  flowId: string;
};

const foo = (cols: (ECol | FCol)[]) => {
  const semiStructuredCols = cols.map((col) =>
    col.type === "element"
      ? {
          ...col,
          cells: col.cells.map((c) =>
            c.type === "flow" ? { ...c, flows: new Array<{ flowId: string }>() } : c,
          ),
        }
      : col,
  );

  return semiStructuredCols.map((col) =>
    col.type === "element"
      ? {
          ...col,
          cells: col.cells.map((c) => c.type === "element" ? c : {...c, flows: c.flows.map(f => f)}),
        }
      : col,
  );
};

🙁 Actual behavior

Example 1 and example 2 fails, but example 3 passes.

Example 1:

Property 'flows' does not exist on type 'FCell'.
Parameter 'f' implicitly has an 'any' type.

Example 2:

Property 'cells' does not exist on type 'FCol | (Omit<ECol, "cells"> & { cells: (ECell | (FCell & { flows: { flowId: string; }[]; }))[]; })'.
Property 'cells' does not exist on type 'FCol'.
Parameter 'c' implicitly has an 'any' type.
Parameter 'f' implicitly has an 'any' type.

🙂 Expected behavior

Both example 1, example 2 and example 3 should pass.

Additional information about the issue

No response

@SimonSimCity SimonSimCity changed the title Overriding type of union works as declared type but not as Overriding type of union works as declared type but not as generic type May 19, 2025
@RyanCavanaugh
Copy link
Member

The problem is that we don't have any type spread operator that could be used to represent this block

  const semiStructuredCols = cols.map((col) =>
    col.type === "element"
      ? {
          ...col,
          cells: col.cells.map((c) =>
            c.type === "flow" ? { ...c, flows: new Array<{ flowId: string }>() } : c,
          ),
        }
      : col,
  );

so instead at col.cells we get this type, which is an intersection of two unions

(ECell | FCell)[] & (ECell | (FCell & {
    flows: any;
}))[]

whose map function is thus overloaded and we pick the first signature (the one that lacks the flows intersection).

I can't really think of any way to fix this except add a generic-spread operator, so this is effectively a duplicate of #10727

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label May 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

2 participants