I want generic object array to one object.
But, It generate union type.
How can I get one merged object type? like {key: string; boo?: string; foo?: string;}
type PluginType<T extends Record<string, unknown> = {}> = T & {
key: string;
}
function renderer<T extends Record<string, unknown> = {}>(plugins?: Array<PluginType<T>>) {
const test = {} as T;
return () => test;
}
// ---cut---
const plugins1: PluginType<{boo?: string}> = { key: '', boo: '' };
const plugins2: PluginType<{foo?: string}> = { key: '', foo: '' };
const render = renderer([plugins1, plugins2]);
render().boo
render().foo
CodePudding user response:
If you have a value v
of type V
and a value w
of type W
, then the array [v, w]
is the type Array<V | W>
, where the element type is a union (since each element is either a V
or a W
); it is not of type Array<V & W>
where the element type is an intersection (which would imply that each element is both a V
and a W
, which is untrue). So you can't just infer the element type from an array and then use it as the merged thing directly.
Instead, what you want to do is to interpret the type of plugins
as a tuple type so the compiler knows the type of each element separately, and then use some type magic to combine the elements of the tuple type into a single type via intersection. Here's one way:
type TupleToIntersection<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void }[number] extends
(x: infer I) => void ? I : never;
declare function
renderer<T extends PluginType<Record<string, unknown>>[]>(
plugins?: [...T]):
() => TupleToIntersection<T>;
So renderer()
is generic in T
, the type of plugins
. Note that I've marked plugins
as having type [...T]
instead of T
. That's variadic tuple type syntax, and it is just there to give the compiler a hint that it should interpret plugins
as being a tuple and not an unordered array.
Once we have T
, we compute TupleToIntersection<T>
to be the intersection of the element types of T
. (So if T
is [V, W, X, Y]
, then TupleToIntersection<T>
is V & W & X & Y
.) This is done by using distributive conditional types and conditional type inference. It's similar to how the UnionToIntersection<T>
type works: I take each element of the tuple and map it to a function parameter type ([V, W, X, Y]
becomes [(x: V)=>void, (x: W)=>void, (x: X)=>void, (y: Y=>void)]
), which is a contravariant position. Then I merge that into a union of functions (((x: V) => void)|((x: W) => void)|((x: X) => void)|((x: Y) => void)
), and from there, infer a single function parameter type that matches it. A union of functions accepts an intersection of parameters, by contravariance, and so the compiler infers (x: infer I) => void
(where I
in the example would become V & W & X & Y
). If that's too confusing then... uh, it's magic