richbai90
richbai90

Reputation: 5204

Combining types in a function dynamically

I'm struggling with how to type this function. I have a function that can take n parameters of type ModelOf<T, A> and returns type of ModelOf<T_n1 & T_n2 &T_n3..., A_n1 | A_n2 | A_n3...> Where T_n and A_n are the generic parameters for the models passed into the function. Here is what I have so far.

export interface ModelOf<T, A extends string> {
  initial: Record.Factory<T>;
  actions: (
    update$: Subject<
      (state: Record<T> & Readonly<T>) => Record<T> & Readonly<T>
    >
  ) => {
    [p in A]: (...P: any) => void;
  };
}

const modelA : ModelOf<ShapeOfModelA, ActionsOfModelA> = ...
const modelB : ModelOf<ShapeOfModelB, ActionsOfModelB> = ...

const combineModels = (...models : ModelOf<any, any>) : ModelOf<any, any> => {...}
const model = combineModels(modelA, modelB); // ModelOf<any, any> is not what I want

It works but I lose type safety along the way. Is there a way to accomplish this without using any

Upvotes: 1

Views: 48

Answers (1)

ford04
ford04

Reputation: 74690

You could write the combineModels function type like this:

declare function combineModels<T extends ModelOf<any, any>[]>(
  ...models: T
): T extends ModelOf<infer I, infer U>[]
  ? ModelOf<UnionToIntersection<I>, U>
  : never;

type UnionToIntersection<T> = (T extends any
  ? (arg: T) => void
  : never) extends (arg: infer I) => void
  ? I
  : never;

, which makes use of generic rest parameters to infer the tuple type for the models function parameter. All instantiations of T and A in ModelOf<T, A extends string> are then conditionally inferred as union types I and U for the return type. We are done, after having converted I (the union of all Ts) to an intersection type.

Test it given some models:

interface ModelOf<T, A extends string> {}
declare const modelA: ModelOf<{ a: number }, "fooAction">;
declare const modelB: ModelOf<{ b: string }, "barAction">;

const model = combineModels(modelA, modelB); 
// ModelOf<{a: number} & {b: string}, "fooAction" | "barAction">

Playground

Hope, it helps!

Update: You can also write it with a lookup type instead (maybe even a bit easier):

declare function combineModels<T extends ModelOf<any, any>[]>(
  ...models: T
): ModelOf<UnionToIntersection<T[number]["t"]>, T[number]["a"]>

Playground

Upvotes: 2

Related Questions