Home > Blockchain >  How to merge of many generic in typescript?
How to merge of many generic in typescript?

Time:10-28

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

TypeScript Playground

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.

Playground link to code

  • Related