Repository navigation
Overriding type of union works as declared type but not as generic type #61729
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 BugsandGenerics.
⏯ Playground Link
💻 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
[-]Overriding type of union works as declared type but not as[/-][+]Overriding type of union works as declared type but not as generic type[/+]RyanCavanaugh commentedon May 19, 2025
The problem is that we don't have any type spread operator that could be used to represent this block
so instead at
col.cellswe get this type, which is an intersection of two unionswhose
mapfunction is thus overloaded and we pick the first signature (the one that lacks theflowsintersection).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
typescript-bot commentedon May 22, 2025
This issue has been marked as "Design Limitation" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
SimonSimCity commentedon May 22, 2025
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
this would be more appropreate:
In a similar way as I do it in the code, where a later definition of a key will overwrite a former.