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") inPzu erhalten. Dies wird mit einem weiteren gemappten TypIndexOfgemacht, der selbst einen dritten gemappten TypIndexesverwendet. - Dann werden die Objekt-Typwerte durch
V[IndexOf<P, K>]angegeben, was nur akzeptabel ist, wennIndexOf<P, K>ein Index vonV(für „Values“) ist, daher der bedingte TypIndexOf<P, K> extends keyof V ? V[IndexOf<P, K>] : never. Wir werden niemalsneverhaben, da sowohl die Array-TypenPals auchVdie gleiche Länge haben, aufgrund der EinschränkungV 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 inFactoryFunctionResultnützlich, um sicherzustellen, dass jedesP0..P2stringerweitert, andernfalls erhalten wir einen TS-Fehler. Abgesehen davon ist es nur ein Alias für ein Objekt mit einer Eigenschaft. Zum Beispiel ergibtMonoRecord<'a', number>{ a: number }.- Der Utility-Typ
Prettify<T>ist nützlich, um Schnittmengentypen zu verschönern. Zum Beispiel ergibtPrettify<{ 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]>> :
