TypeScript Generic Factory Function Type, matching array element order
Asked Answered
M

2

5

I want to build some kind of FactoryFactory: Basically a generic function that returns a factory function. Writing the function itself is simple, but I can't figure out how to do the TypeScript typings for it.

The function should be used like this:

const stubFactoryFunction = (...props) => (...values) => ({ /* ... */ });

const factory = stubFactoryFunction("prop1", "prop2");
const instance = factory("foo", 42);
console.log(instance); // { prop1: "foo", prop2: 42 }

At first I tried to provide the value types as an array:

type FactoryFunction<T extends any[]> =
  <K extends string[]>(...props: K) =>
    (...values: T[number]) =>
      {[key in K[number]]: T[number]}

But this will result in { prop1: string | number, prop2: string | number}, because the type doesn't match the array indexes.

Next I tried to provide the whole object as generic type:

type FactoryFunction<T extends {[key: string]: any}> =
  (...props: (keyof T)[]) =>
    (...values: ???) =>
      T

And here I got a similar problem: values must somehow match the order of props.

Is this possible at all?

Bonus 1: Don't allow duplicate props.

Bonus 2: Enforce the provide all non-optional props from T.

Mall answered 10/8, 2020 at 13:17 Comment(0)
P
5

This is another solution, generic this time and finally not so complicated, eventhough some intermediary types are necessary. The base ideas are:

  • Having an object type with keys in a string tuple type P (for "Props") (extends string[]) is done with a mapped type { [K in P[number]]: ... }
  • The difficulty is to get the index of K (one of the "Props") in P. It's done using another mapped type IndexOf, itself using a third mapped type Indexes.
  • Then, the object type values are given by V[IndexOf<P, K>] which can be acceptable only if IndexOf<P, K> is an index of V (for "Values"), hence the conditional type IndexOf<P, K> extends keyof V ? V[IndexOf<P, K>] : never. We will never have never since both P and V array types have the same length due to the constraint V extends (any[] & { length: P['length'] }).
// Utility types
type Indexes<V extends any[]> = {
  [K in Exclude<keyof V, keyof Array<any>>]: K;
};

type IndexOf<V extends any[], T> = {
  [I in keyof Indexes<V>]: V[I] extends T ? T extends V[I] ? I : never : never;
}[keyof Indexes<V>];

type FactoryFunctionResult<P extends string[], V extends (any[] & { length: P['length'] })> = {
  [K in P[number]]: IndexOf<P, K> extends keyof V ? V[IndexOf<P, K>] : never;
};

// Tests
type IndexesTest1 = Indexes<['a', 'b']>; // { 0: "0"; 1: "1" }

type IndexOfTest1 = IndexOf<['a', 'b'], 'b'>; // "1"
type IndexOfTest2 = IndexOf<['a', 'b'], string>; // never
type IndexOfTest3 = IndexOf<[string, string], 'a'>; // never
type IndexOfTest4 = IndexOf<[string, string], string>; // "0" | "1"

type FactoryFunctionResultTest1 = FactoryFunctionResult<['a'], [string]>; // { a: string }
type FactoryFunctionResultTest2 = FactoryFunctionResult<['a', 'b'], [string, number]>; // { a: string; b: number }
type FactoryFunctionResultTest3 = FactoryFunctionResult<['a', 'b', 'c'], [string, number, boolean]>; // { a: string; b: number; c: boolean }
type FactoryFunctionResultTest4 = FactoryFunctionResult<['a', 'b', 'c', 'd'], [string, number, boolean, string]>; // { a: string; b: number; c: boolean; d: string }
Plashy answered 13/8, 2020 at 9:26 Comment(2)
Love it how you use length property to enforce the same length of P and V!Pejoration
I don't understand how this works but I do understand what it does and intend to use itTeeters
P
4

I don't know if it's feasible in a generic way, since mapped types enable to map values, not keys of the input object, which is necessary here.

A solution for a limited number of arguments, here from 1 to 3:

type MonoRecord<P, V> = P extends string ? Record<P, V> : never;

type Prettify<T> = T extends infer Tbis ? { [K in keyof Tbis]: Tbis[K] } : never;

type FactoryFunctionResult<
  P extends (string[] & { length: 1|2|3 }),
  V extends (any[] & { length: P['length'] })
> =
  P extends [infer P0] ?
    MonoRecord<P0, V[0]> :
  P extends [infer P0, infer P1] ?
    Prettify<
      MonoRecord<P0, V[0]> &
      MonoRecord<P1, V[1]>> :
  P extends [infer P0, infer P1, infer P2] ?
    Prettify<
      MonoRecord<P0, V[0]> &
      MonoRecord<P1, V[1]> &
      MonoRecord<P2, V[2]>> :
    never;

type FactoryFunctionTest1 = FactoryFunctionResult<['a'], [string]>; // { a: string }
type FactoryFunctionTest2 = FactoryFunctionResult<['a', 'b'], [string, number]>; // { a: string; b: number }
type FactoryFunctionTest3 = FactoryFunctionResult<['a', 'b', 'c'], [string, number, boolean]>; // { a: string; b: number; c: boolean }

const stubFactoryFunction = <P extends (string[] & { length: 1|2|3 })>(...props: P) =>
    <V extends (any[] & { length: P['length'] })>(...values: V) =>
        ({ /* ... */ } as FactoryFunctionResult<P, V>);

Some explanations:

  • Number of arguments are limited to 3 due to & { length: 1|2|3 } constraint.
  • The inner factory must have the same number of arguments (values) than the outer factory arguments (props) due to & { length: P['length'] }.
  • MonoRecord<P, V> is useful in FactoryFunctionResult to ensure each P0..P2 extends string otherwise we get a TS error. Apart from that, it's just an alias for an object with one property. E.g. MonoRecord<'a', number> gives { a: number }.
  • Prettify<T> utility type is useful to prettify intersection types. E.g. Prettify<{ a: number } & { b: string }> gives { a: number; b: string }.

To have up to 4 arguments:

  • Change constraint length: 1|2|3|4
  • Add a fourth case in FactoryFunctionResult formula:
  P extends [infer P0, infer P1, infer P2, infer P3] ?
    Prettify<
      MonoRecord<P0, V[0]> &
      MonoRecord<P1, V[1]> &
      MonoRecord<P2, V[2]> &
      MonoRecord<P3, V[3]>> :
Plashy answered 11/8, 2020 at 12:14 Comment(2)
Thanks for the answer. This looks pretty complicated, but I see what you did there. I still hope that someone comes across some more general approach.Mall
I've posted another solution, this time generic (no limit on argument count). If this is still to much complicated, perhaps @jcalz (best utility type maker) can give us a cleaner solution.Plashy

© 2022 - 2024 — McMap. All rights reserved.