There's a lot going on here; the short answer is that the compiler really isn't equipped to verify type safety in the face of correlated union typed expressions. Consider this code:
declare const [a, b]: ["T", "X"] | ["U", "Y"]; // okay
const oops: ["T", "X"] | ["U", "Y"] = [a, b]; // error!
Here the variables a and b are correlated: if a is "T" then b must be "X". Otherwise a is "U" and b is "Y". The compiler sees a as type "T" | "U" and b as type "X" | "Y", which are both true. But by doing this it has failed to keep track of the correlation between them. And so [a, b] is considered to be of type ["T", "X"] | ["T', "Y"] | ["U", "X"] | ["U", "Y"]. And you get errors.
This is what's happening to you, more or less. I can rewrite your code into a non-generic version like this:
function fooEither(parents: Left[] | Right[], child: LeftChild | RightChild) {
parents[0].child = child; // no error, but unsafe!
parents.push({ child }) // error
}
fooEither([{ child: { element: 0 } }], { element: 1 }); // no error at compile time
Here you can see that the compiler is happy with parents[0].child = child, which it shouldn't be... and rightly unhappy about parents.push({ child }). There's nothing that restricts parents and child to be properly correlated.
Why does parents[0].child = child work? Well, the compiler largely does allow unsound property writes. See microsoft/TypeScript#33911 for example, along with discussion around why they've decided to keep it that way.
I could try to rewrite the above code to enforce the correlation at the call site, but the compiler still can't see it inside the implementation:
function fooSpread(...[parents, child]: [Left[], LeftChild] | [Right[], RightChild]) {
parents[0].child = child; // same no error
parents.push({ child }) // same error
}
fooSpread([{ child: { element: 0 } }], { element: 1 }); // not allowed now, that's good
Without some way for the compiler to maintain correlations among union types, there's not much to be done here.
The two workarounds I've seen here is to duplicate code (much like fooLeft() and fooRight()) or to use type assertions to just silence the compiler:
function fooAssert<T extends keyof Typings>(parents: Typings[T]["parent"][], child: Typings[T]["child"]) {
parents[0].child = child
parents.push({ child } as Typings[T]["parent"]); // no error now
}
This compiles but only because you have taken the responsibility of telling the compiler that what you are doing is safe, not the other way around. And it's dangerous because assertions make it more likely that you have done something unsafe without realizing it:
fooAssert([{ child: { element: 0 } }], { element: 1 }); // look ma no error
// fooAssert<"left"|"right">(...);
Oops, what happened? Well, your call signature is generic in T but there is no parameter of type T passed in. The compiler fails to infer anything for T and instead just widens it to its constraint, which is "left" | "right". And thus the call signature of foo() and fooAssert() is the same unsafe one as in fooEither(). In this case if I were you I might fall back to something like fooSpread() which at least is safe to call.
The issue of correlated values has come up often enough that I've filed an issue, microsoft/TypeScript#30581, suggesting that it would be nice to have something better than duplication or assertion here. Alas, nothing obvious is forthcoming.
Okay, hope that helps; good luck!
Playground link to code