Skip to content

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

Closed as not planned

Description

🔎 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

Activity

  1. 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[/+] on May 19, 2025
  2. RyanCavanaugh commented on May 19, 2025

    @RyanCavanaugh
    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

  3. typescript-bot commented on May 22, 2025

    @typescript-bot
    Contributor

    This issue has been marked as "Design Limitation" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  4. SimonSimCity commented on May 22, 2025

    @SimonSimCity
    Author

    Ryan Cavanaugh (@RyanCavanaugh), sorry for answering on this auto-closed ticket, but IMO, one solution would be to flip the order of the types. Instead of

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

    this would be more appropreate:

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

    In a similar way as I do it in the code, where a later definition of a key will overwrite a former.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions