type PluginType<
Meta extends Record<string, unknown> = {},
Field extends Record<string, unknown> = {}
> = {
key: string;
getMeta?(meta: Meta): void
getField?(meta: Field): void
};
I want to return merged Meta
and Field
of PluginType
to render
function.
The render
function takes PluginType[]
as an argument.
declare function render<
T extends PluginType[] // Is this ok?
>( options?: { plugins: T }): {
meta: // merge T.Meta
field: // merge T.Field
};
const p1: PluginType<
{ metaBoo?: string },
{ fieldBoo?: string }
> = { key: 'p1' };
const plugins2: PluginType<
{ metaFoo?: string },
{ fieldFoo?: string }
> = { key: 'p2'};
const { meta, field } = render([p1, p2]);
In the above situation, I want the return value of render
to come out like the one below.
{
meta: { metaBoo?: string, metaFoo?: string },
field: { fieldBoo?: string, fieldFoo?: string }
}
How can I specify the return type of the render
function for this?
I posted a similar question yesterday and attached it. TypeScript merge generic array
CodePudding user response:
It looks like you want this:
declare function renderer<
T extends PluginType[]
>(options?: { plugins: [...T] }): {
meta?: TupleToIntersection<{ [I in keyof T]:
T[I] extends PluginType<infer M, any> ? M : never
}>,
field?: TupleToIntersection<{ [I in keyof T]:
T[I] extends PluginType<any, infer F> ? F : never
}>
};
type TupleToIntersection<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void }[number] extends
(x: infer I) => void ? I : never;
It is similar to the other question where I defined TupleToIntersection<T>
to take a tuple type like [A, B, C, D]
and compute the intersection of its elements like A & B & C & D
.
In this case you expect the output of renderer({options: pluginTuple})
where pluginTuple
is of tuple type [PluginType<M1, F1>, PluginType<M2, F2>, PluginType<M3, F3>]
to be of type {meta?: M1 & M2 & M3; field?: F1 & F2 & F3}
. So before we use TupleToIntersection
, we need to convert the type of pluginTuple
to [M1, M2, M3]
(for the meta
property) and to [F1, F2, F3]
(for the field
property).
To do this we can make a mapped tuple type; if you have a generic tuple type T
, then the mapped type {[I in keyof T]: ...T[I]...}
will also be a tuple type where each element I
of the input tuple T
is converted to some corresponding output element type. And to convert PluginType<M, F>
to either M
or F
, we can use [conditional type inference with infer
]. Such as {[I in keyof T]: T[I] extends PluginType<infer M, any> ? M : never}
or {[I in keyof T]: T[I] extends PluginType<any, infer F> ? F : never}
Let's test it out:
const plugins1: PluginType<{ metaBoo?: string }, { fieldBoo?: string }> = { key: '' };
const plugins2: PluginType<{ metaFoo?: string }, { fieldFoo?: string }> = { key: '' };
const render = renderer({
plugins: [plugins1, plugins2]
});
/* const render: {
meta?: ({
metaBoo?: string | undefined;
} & {
metaFoo?: string | undefined;
}) | undefined;
field?: ({
fieldBoo?: string | undefined;
} & {
fieldFoo?: string | undefined;
}) | undefined;
} */
Looks good. render
is an object type with an optional property meta
of type {metaBoo?: string} & {metaFoo?: string}
, and an optional property field
of type {fieldBoo?: string} & {fieldFoo?: string}
.
So there you go. There are some caveats that this only works as well as conditional type inference can work. If you explicitly annotate a type as PluginsType<M, F>
then we can probably infer
M
and F
from it. But if you just write a value and expect the compiler to infer that it is of type PluginsType<M, F>
then you might be disappointed. I note that your PluginType<M, F>
definition has M
and F
in optional properties, so they might not be present at all, and they are in a contravariant position, so they might be inferred in a way you don't expect. Dealing with either of these is outside the scope of the question as asked, but it's important to note that infer
is not magic and has its limits.