You can just declare a type parameter for the array item type and return it as the return value for the function, and everything will work out for your particular example because of the way typescript infers the type for the array (it will be { foo: string; bar?: undefined; } | { bar: string; foo?: undefined;})[]
). But to make sure that any union is flattened correctly we will need a bit of extra help from this answer on UnionToIntersection
type UnionToIntersection<U> = (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
const array = [{ foo: 'val1' }, { bar: 'val2' }]
function flatten<T> (array: T[]) : UnionToIntersection<T> {
// implementation here, does not matter, all that matter is that it returns following:
return {foo: 'val1', bar: 'val2'} as any
}
// at this point IDE suggests two values: "foo" and "bar"
flatten(array).bar
flatten(array).foo
Edit
The reason to use UnionToIntersection<T>
is because the type of the array item very much depends on how you define the array:
Ex:
const array1 = [{foo: 'val1'}, {bar: 'val2'}] // { foo: string; bar?: undefined; } | { bar: string; foo?: undefined;})[]`)
const foo = { foo: 'val1' }
const bar = { bar: 'val2' }
const array2 = [foo, bar]// ({ foo: string; } | { bar: string; })[]
For array1
typescript will add properties of all object literals to all types in the union, we get a bunch of extra optional properties with type undefined
on each member of the union ({ foo: string; bar?: undefined; }
) And this is why we can access any member even though they are not present on all items in the array.
For array2
, the type of each item is already known (ie decided before the array definition) so typescript will not add those extra properties and we get a union with no common members as the type for an item of the array ({ foo: string; } | { bar: string; }
) and we will not be able to access any member.