If you want to restrict Grandparent's children property to be an array of just known extensions of Parent, then a union is the only way. If, on the other hand, you wish to accept Parent and all structurally compatible extensions, even ones that haven't been declared yet... then you don't need to change the definition at all.
Your existing Grandparent definition already allows the elements of the children property to be a Parent or any subtype of Parent like ChildA or ChildB. The issue you might be running into is that, when you use an object literal like you did with concrete, the compiler performs excess property checking and complains about properties it doesn't expect:
const concrete: Grandparent = {
children: [
{
sharedProp: 'fooA',
uniqueChildAProp: 'barA' // error!
},
{
sharedProp: 'foo',
uniqueChildBProp: 'barB' // error!
}
]
};
But that doesn't mean that you can't pass a ChildA or ChildB; it just means that you need to tell the compiler that you intend to have a ChildA or ChildB so that the properties are not unexpected:
const cA: ChildA = { sharedProp: 'fooA', uniqueChildAProp: 'barA' };
const cB: ChildB = { sharedProp: 'foo', uniqueChildBProp: 'barB' };
const concrete1: Grandparent = { children: [cA, cB] };
Or you can refactor in a number of other ways to appease the compiler and avoid excess property checking:
const children = [
{
sharedProp: 'fooA',
uniqueChildAProp: 'barA'
},
{
sharedProp: 'foo',
uniqueChildBProp: 'barB'
}
];
const concrete2: Grandparent = { children }; // okay
If you want to allow subtypes of Parent without complaining about extra properties in object literals, you might decide to use an index signature because object literals are implicitly compatible with appropriate index signatures. Like this:
interface AnyParent extends Parent {
[k: string]: unknown;
}
interface Grandparent2 {
children: AnyParent[];
}
const concrete3: Grandparent2 = {
children: [
{
sharedProp: 'fooA',
uniqueChildAProp: 'barA'
},
{
sharedProp: 'foo',
uniqueChildBProp: 'barB'
}
]
}; // okay
That works for your example code. Be careful though; according to microsoft/TypeScript#15300, interface types are not implicitly compatible with index signatures, so using an explicit ChildA annotation will no longer work:
const childA: ChildA = { sharedProp: "", uniqueChildAProp: "" };
const oops: Grandparent2 = { children: [childA] }; // error!
So an index signature is really only advisable if you only care about object literals and type aliases and not interfaces. Therefore I'd probably suggest the first option and refactor, annotate, or assert that you are intentionally adding extra properties.
Okay, hope that helps; good luck!
Playground link to code