Weboldal keresés

typescript - A generikus objektum visszatérési típusa a metódusláncolás eredménye


Szeretném a következőt tenni:

var result = loader
    .add<number>(1)
    .add<string>("hello")
    .add<boolean>(true)
    .run();

Szeretném úgy felépíteni ezt az elméleti loader objektumot, hogy az eredmény típusa [number, string, boolean] legyen anélkül, hogy manuálisan kellene deklarálnom így. Van erre mód a TypeScript-ben?

Válasz #1

FRISSÍTÉS: A TypeScript 4.0 változatos tuple típusokat fog tartalmazni, amelyek lehetővé teszik a beépített tuple-ok rugalmasabb kezelését. A Push<T, V> egyszerűen így valósítható meg: [...T, V]. Ezért az egész implementáció a következő viszonylag egyszerű kódrészletté válik:

type Loader<T extends any[]> = {
    add<V>(x: V): Loader<[...T, V]>;
    run(): T
}
declare const loader: Loader<[]>;

var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]

Játszótér hivatkozás


TS v4.0 előtt:

Sajnos a TypeScript-ben nincs támogatott mód arra, hogy a típusműveletet, amely egy típust fűz a tuple végéhez, reprezentáljuk. Ezt a műveletet Push<T, V> néven fogom hívni, ahol T egy tuple, és V bármilyen értéktípus. Van azonban mód arra, hogy egy értéket a tuple elejéhez hozzáfűzzünk, amit Cons<V, T> néven fogok hívni. Ennek oka, hogy a TypeScript 3.0-ban bevezetésre került egy funkció, amely lehetővé teszi a tuple-ök kezelését a függvényparaméterek típusaiként. Emellett megszerezhetjük a Tail<T>-t is, amely eltávolítja a tuple első elemét (a head-et), és visszaadja a maradékot:

type Cons<H, T extends any[]> = 
  ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never;
type Tail<T extends any[]> = 
  ((...x: T) => void) extends ((h: infer A, ...t: infer R) => void) ? R : never;

Adott a Cons és a Tail, a Push természetes reprezentációja ez az rekurzív dolog lenne, ami nem működik:

type BadPush<T extends any[], V> = 
  T['length'] extends 0 ? [V] : Cons<T[0], BadPush<Tail<T>, V>>; // error, circular

Az elképzelés ott az, hogy a Push<[], V> egyszerűen [V] legyen (üres tuple-höz hozzáfűzni könnyű), és a Push<[H, ...T], V> pedig Cons<H, Push<T, V>> (megőrzöd az első elemet H, és egyszerűen hozzáfűzöd V-t a farokhoz T... majd H-t visszateszed az eredmény elé).

Bár lehetséges becsapni a fordítót, hogy engedje az ilyen rekurzív típusokat, ez nem ajánlott. Amit általában helyette csinálok, az az, hogy kiválasztok egy bizonyos maximális ésszerű hosszúságú tuple-t, amelyet módosítani szeretnék (például 9 vagy 10), majd kibontom a körkörös definíciót:

type Push<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push1<Tail<T>, V>>
type Push1<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push2<Tail<T>, V>>
type Push2<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push3<Tail<T>, V>>
type Push3<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push4<Tail<T>, V>>
type Push4<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push5<Tail<T>, V>>
type Push5<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push6<Tail<T>, V>>
type Push6<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push7<Tail<T>, V>>
type Push7<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push8<Tail<T>, V>>
type Push8<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push9<Tail<T>, V>>
type Push9<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], PushX<Tail<T>, V>>
type PushX<T extends any[], V> = Array<T[number] | V>; // give up

Minden sor kivéve a PushX-et pontosan úgy néz ki, mint a rekurzív definíció, és szándékosan megszakítjuk a folyamatot a PushX-nél azzal, hogy feladjuk, és egyszerűen elfelejtjük az elemek sorrendjét (PushX<[1,2,3],4> az Array<1 | 2 | 3 | 4> lesz).

Most ezt megtehetjük:

type Test = Push<[1, 2, 3, 4, 5, 6, 7, 8], 9> // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Felfegyverkezve a Push-szal, adjunk típust a loader-nek (a megvalósítást rád bízzuk):

type Loader<T extends any[]> = {
  add<V>(x: V): Loader<Push<T, V>>;
  run(): T
}
declare const loader: Loader<[]>;

És próbáljuk ki:

var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]

Jól néz ki. Remélem, ez segít; sok szerencsét!


FRISSÍTÉS

A fenti csak a --strictFunctionTypes engedélyezésével működik. Ha mindenképpen el kell hagynod ezt a fordítókapcsolót, a következő Push definíciót használhatod helyette:

type PushTuple = [[0], [0, 0], [0, 0, 0],
    [0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
type Push<
    T extends any[],
    V,
    L = PushTuple[T['length']],
    P = { [K in keyof L]: K extends keyof T ? T[K] : V }
    > = P extends any[] ? P : never;

Kisebb támogatott tuple-méretek esetén tömörebb, ami jó, de a ismétlődés a támogatott tuple-ok számának négyzetével nő (O(n2) növekedés) a lineáris növekedés helyett (O(n) növekedés), ami kevésbé jó. Mindenesetre az TS3.1-ben bevezetett leképezett tuple-ok használatával működik.

Rajtad múlik.

Sok szerencsét újra!

Kapcsolódó cikkek