TypeScript generisk fabrikfunktionstyp, matchande arrayelementsordning


Jag vill bygga någon slags FactoryFactory: I princip en generell funktion som returnerar en fabriksfunktion. Att skriva själva funktionen är enkelt, men jag kan inte lista ut hur man gör TypeScript-typerna för den.

Funktionen bör användas så här:

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

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

Först försökte jag ange värdetyperna som en array:

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

Men detta kommer att resultera i { prop1: string | number, prop2: string | number}, eftersom typen inte matchar array-indexen.

Nästa försökte jag att tillhandahålla hela objektet som generisk typ:

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

Och här fick jag ett liknande problem: values måste på något sätt matcha ordningen på props.

Är detta möjligt överhuvudtaget?

Bonus 1: Tillåt inte dubblett-egenskaper.

Bonus 2: Tvinga fram att alla icke-valfria egenskaper från T tillhandahålls.

Svar #1

Detta är en annan lösning, generisk den här gången och slutligen inte så komplicerad, även om några mellanliggande typer är nödvändiga. De grundläggande idéerna är:

  • Att ha en objekttyp med nycklar i en strängtuppeltyp P (för "Props") (extends string[]) görs med en mappad typ { [K in P[number]]: ... }
  • Svårigheten är att få indexet för K (en av "Props") i P. Det görs med hjälp av en annan avbildad typ IndexOf, som själv använder en tredje avbildad typ Indexes.
  • Sedan ges objekttypvärdena av V[IndexOf<P, K>], vilket bara kan accepteras om IndexOf<P, K> är ett index av V (för "Values"), därav den konditionella typen IndexOf<P, K> extends keyof V ? V[IndexOf<P, K>] : never. Vi kommer aldrig att ha never eftersom både P och V arraytyper har samma längd på grund av begränsningen 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 }

Svar #2

Jag vet inte om det är genomförbart på ett generellt sätt, eftersom mapade typer möjliggör att mappa värden, inte nycklarna i ingångsobjektet, vilket är nödvändigt här.

En lösning för ett begränsat antal argument, här från 1 till 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>);

Några förklaringar:

  • Antalet argument är begränsat till 3 på grund av & { length: 1|2|3 }-begränsningen.
  • Den inre fabriken måste ha samma antal argument (values) som de yttre fabrikens argument (props) på grund av & { length: P['length'] }.
  • MonoRecord<P, V> är användbar i FactoryFunctionResult för att säkerställa att varje P0..P2 utökar string, annars får vi ett TS-fel. Förutom det är det bara ett alias för ett objekt med en egenskap. T.ex. MonoRecord<'a', number> ger { a: number }.
  • Utility-typen Prettify<T> är användbar för att försköna snittyp-typer. T.ex. Prettify<{ a: number } & { b: string }> ger { a: number; b: string }.

Att ha upp till 4 argument:

  • Ändra begränsning length: 1|2|3|4
  • Lägg till ett fjärde fall i FactoryFunctionResult-formeln:
  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]>> :

Relaterade artiklar