Website-Suche

TypeScript Generische Fabrikfunktion Typ, Übereinstimmung der Reihenfolge der Array-Elemente


Ich möchte eine Art FactoryFactory bauen: Im Grunde eine generische Funktion, die eine Fabrikfunktion zurückgibt. Die Funktion selbst zu schreiben ist einfach, aber ich kann nicht herausfinden, wie man die TypeScript-Typisierung dafür macht.

Die Funktion sollte so verwendet werden:

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

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

Zuerst versuchte ich, die Werttypen als ein Array bereitzustellen:

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

Aber dies wird zu { prop1: string | number, prop2: string | number} führen, weil der Typ nicht zu den Array-Indexen passt.

Als Nächstes versuchte ich, das gesamte Objekt als generischen Typ bereitzustellen:

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

Und hier habe ich ein ähnliches Problem: values muss irgendwie mit der Reihenfolge von props übereinstimmen.

Ist das überhaupt möglich?

Bonus 1: Erlaube keine doppelten Requisiten.

Bonus 2: Erzwinge, dass alle nicht optionalen Props von T bereitgestellt werden.

Antwort #1

Dies ist eine weitere Lösung, diesmal allgemein und schließlich nicht so kompliziert, obwohl einige Zwischentypen notwendig sind. Die Grundideen sind:

  • Das Vorhandensein eines Objekttyps mit Schlüsseln in einem String-Tupel-Typ P (für „Props“) (extends string[]) wird mit einem abgebildeten Typ { [K in P[number]]: ... } durchgeführt
  • Die Schwierigkeit besteht darin, den Index von K (einem der "Props") in P zu erhalten. Dies wird mit einem weiteren gemappten Typ IndexOf gemacht, der selbst einen dritten gemappten Typ Indexes verwendet.
  • Dann werden die Objekt-Typwerte durch V[IndexOf<P, K>] angegeben, was nur akzeptabel ist, wenn IndexOf<P, K> ein Index von V (für „Values“) ist, daher der bedingte Typ IndexOf<P, K> extends keyof V ? V[IndexOf<P, K>] : never. Wir werden niemals never haben, da sowohl die Array-Typen P als auch V die gleiche Länge haben, aufgrund der Einschränkung 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 }

Antwort #2

Ich weiß nicht, ob es auf generische Weise machbar ist, da gemappte Typen es ermöglichen, Werte zuzuordnen, nicht die Schlüssel des Eingabeobjekts, was hier notwendig ist.

Eine Lösung für eine begrenzte Anzahl von Argumenten, hier von 1 bis 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>);

Einige Erklärungen:

  • Die Anzahl der Argumente ist aufgrund der & { length: 1|2|3 }-Einschränkung auf 3 begrenzt.
  • Die innere Fabrik muss die gleiche Anzahl von Argumenten (values) wie die äußeren Fabrikargumente (props) haben aufgrund von & { length: P['length'] }.
  • MonoRecord<P, V> ist in FactoryFunctionResult nützlich, um sicherzustellen, dass jedes P0..P2 string erweitert, andernfalls erhalten wir einen TS-Fehler. Abgesehen davon ist es nur ein Alias für ein Objekt mit einer Eigenschaft. Zum Beispiel ergibt MonoRecord<'a', number> { a: number }.
  • Der Utility-Typ Prettify<T> ist nützlich, um Schnittmengentypen zu verschönern. Zum Beispiel ergibt Prettify<{ a: number } & { b: string }> { a: number; b: string }.

Bis zu 4 Argumente haben:

  • Ändere die Einschränkung length: 1|2|3|4
  • Fügen Sie eine vierte Variante in die FactoryFunctionResult-Formel ein:
  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]>> :

Verwandte Artikel