There are two problems here.
The first one has to do with excess property checks when unions are involved. You can read this answer here to a similar question. The gist of it is that excess property checks for unions allows any key of any member to be present on the object. We can get around this by introducing extra members of type never to make sure that the object with excess properties in not wrongly compatible with a particular member:
type SidebarItems = Array<StrictUnion<SimpleSidebarItem | ComplexSidebarItem>>;
type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
const items2: SidebarItems = [{title: '', url: '', subItems: [{title: '', url: ''}]}]; //error
const items3: SidebarItems = [{title: '', subItems: [{title: '', url: ''}]}]; //ok
The second problem is related to the fact that typescript will not do any extra inference if you specify the type of a variable, so the information that items[1]
is ComplexSidebarItem
is lost, all typescript will know is that an item can be SimpleSidebarItem | ComplexSidebarItem
.
We can either use a type guard to check the type:
const items: SidebarItems = [{title: '', url: ''}, {title: '', subItems: [{title: '', url: ''}]}];
const shouldBeComplexSidebarItem = items[1];
if(!('url' in shouldBeComplexSidebarItem)){ //type guard
shouldBeComplexSidebarItem.subItems // is ComplexSidebarItem here
}
Or we could use a function to create the array that will infer a tuple type, for which types as at a particular index are known:
function createItems<T extends SidebarItems>(...a:T){
return a;
}
const items = createItems({title: '', url: ''}, {title: '', subItems: [{title: '', url: ''}]});
const shouldBeComplexSidebarItem = items[1];
shouldBeComplexSidebarItem.subItems // is an object literal compatible with ComplexSidebarItem
You can also manually specify the tuple type, in which case the StrictUnion
is not needed anymore:
const items: [SimpleSidebarItem, ComplexSidebarItem] = [{title: '', url: ''}, {title: '', subItems: [{title: '', url: ''}]}];
const shouldBeComplexSidebarItem = items[1];
shouldBeComplexSidebarItem.subItems // is ComplexSidebarItem