typescript - Tiparea funcției pipe compose care folosește reduceRight cu generice


Acum ceva timp, un utilizator prietenos de pe Stack Overflow m-a ajutat să rezolv cum să fac o funcție compose să funcționeze corect cu genericele TypeScript.

type OuterFunction<OA, OR> = (arg: OA) => OR;
type InnerFunction<IA extends unknown[], IR> = (...args: IA) => IR;

const compose = <T extends unknown[], V, W>(
  outer: OuterFunction<V, W>,
  inner: InnerFunction<T, V>
) => (...innerParams: T) => outer(inner(...innerParams));

Acest lucru funcționează foarte bine. Astfel, funcția compusă rezultată are tipul argumentelor funcției interioare și tipul de returnare al funcției exterioare și așteaptă corect ca argumentele funcției exterioare să se potrivească cu tipul de returnare al funcției interioare.

Și aici este funcția mea pipe care utilizează același compose cu reduceRight, permițând definirea funcțiilor din mai mult de două alte funcții:

const pipe = (...functions: any[]) =>
  functions.reduceRight((prev, curr) => compose(prev, curr));

Acesta rulează și funcționează din punct de vedere JavaScript așa cum era de așteptat. Tipurile sunt, evident, complet dispărute când se procedează astfel.

Mă jucam și încercam mai multe moduri decât pot să-mi amintesc și nici măcar nu are prea mult sens să le listez aici, pentru că niciunul nu a funcționat deloc. Niciunul nu s-a compilat, sau a rezultat doar în tipuri nevalide, la fel ca versiunea de bază de mai sus.

Aici este acest lucru într-un teren de joacă TypeScript al acestuia:
https://tsplay.dev/w62jEw

Cum face fp-ts acest lucru?

M-am uitat la modul în care fp-ts face asta cu pipe-ul lor și am ajuns la concluzia că soluția lor va funcționa foarte bine, dar nu este la fel de plăcută ca folosirea reduceRight (este practic o imensă cascadă de supraîncărcări pe care o fac pentru a realiza acest lucru).

Deoarece conectarea prin pipe a mai mult de 18 funcții împreună nu este de fapt un scenariu pe care trebuie să îl aștept ca fiind destul de comun, experimentarea mea pentru acea soluție este de fapt redundantă.

Dar poate există o soluție bună undeva, și în acest caz ar putea fi interesant ca chiar mai mult decât doar eu să afle despre ea ;)


Mulțumesc anticipat pentru orice sugestii.

Răspuns #1

Cu tipurile tuple variadice, care sunt o caracteristică nouă în TypeScript 4.0, poți face următoarele:

type Func = (...args: any) => any
declare function pipe<T extends Func, U extends Func, R extends Func>
    (...functions: [T, ...U[], R]) : (...args: Parameters<T>) => ReturnType<R>;

Tipurile variaționale Typle îți permit să definești un ...functions array ca [T, ...U[], R] și apoi să folosești primele și ultimele semnături ale funcțiilor (T și R) pentru a extrage parametrii de intrare și de returnare.

Un exemplu complet ar arăta astfel:

const onePlus = (a: number): number => a + 1;
const asString = (a: number): string => `${a}`;
const toInt = (a: string): number => Number.parseInt(a, 10);


type Func = (...args: any) => any
declare function pipe<T extends Func, U extends Func, R extends Func>
    (...functions: [T, ...U[], R]) : (...args: Parameters<T>) => ReturnType<R>;

const pipeWorks = pipe(
  onePlus,
  asString,
  toInt,
  onePlus,
  asString,
  toInt,
  onePlus
);

console.log(pipeWorks(1));

Sau în terenul de joacă TS aici.

Dacă lași doar două funcții pipe:

const pipeWorks = pipe(
  onePlus,
  asString
);

Semnătura funcției pipeWorks va fi (a: number) => string.

Totuși, rețineți că această semnătură a funcției pipe preia doar parametrii de intrare ai primei funcții T și parametrul de returnare al ultimei funcții R. Nu verifică tipurile niciunei funcții intermediare, dacă semnăturile lor (parametrii de intrare/ieșire) pot fi legate sau nu.

Probabil ar trebui să restricționezi puțin mai mult tipul Func, pentru a te asigura că semnăturile de concatenare sunt valide.

Răspunsul #2

După mai multe încercări, am această soluție.

type Func = (...args: any) => any

function pipe<T extends Func, U extends Func[], R extends Func>
    (...fns: [T, ...U, R]): (x: Parameters<T>[number]) => ReturnType<R> {
        return (x) => fns.reduce((y, f) => f(y), x as Parameters<T | U[number] | R>);
    }

const add1 = (x: number) => x + 1;
const add2 = (x: number) => x + 2;
const add3 = (x: number) => x + 3;
const add4 = (x: number) => x + 4;
const add10 = pipe(add1, add2, add3, add4); // all same type works
console.log(add10(2));

const mix = pipe(add1, String, Number); // mix type works
console.log(mix(2));

Poți să plasezi cursorul mouse-ului peste pipe și să vezi inferența corectă a tipului, deoarece prin utilizarea Tipurilor de Tuple Variadice, genericul U este un tuple care conține semnăturile funcțiilor intermediare.

Lecturi suplimentare: Consider că acest blog este foarte bine scris despre Tipurile de Tuple Variadice.

Actualizat:

Dacă vrei ca prima funcție să accepte mai mulți parametri, trebuie să revizuiești corpul funcției ca mai jos, deși tipurile sunt destul de similare

const pipe2 = <T extends Func, U extends Func[], R extends Func>
    (...fns: [T, ...U, R]): (...args: Parameters<T>) => ReturnType<R> => 
        fns.reduce((f, g) => 
            (...args: [...Parameters<T>]) => g(f(...args)));

Totuși, această tehnică nu poate gestiona zero sau un parametru. Trebuie să adăugăm supraîncărcarea funcțiilor pentru a o face mai generală.

type Func = (...args: any) => any
type Noop = () => void;
const noop: Noop = () => {};

function pipe2(): Noop;
function pipe2<T extends Func>(fn: T): T;
function pipe2<T extends Func, U extends Func[], R extends Func>(...fns: [T, ...U, R]): (...args: Parameters<T>) => ReturnType<R>;
function pipe2(...fns: any) {
    if (fns.length === 0) return noop;
    if (fns.length === 1) return fns[0];
    return fns.reduce((f: any, g: any) =>
        (...args: any) => g(f(...args))
    );
}

const addxy = (x: number, y: number) => x + y;
const add1 = (x: number) => x + 1;
const add2 = (x: number) => x + 2;
const add3 = (x: number) => x + 3;
const noopAgain = pipe2();
const addxyAgain = pipe2(addxy);
const addxyAnd1 = pipe2(addxy, add1);
const addxyAnd1And2 = pipe2(addxy, add1, add2);
const addAll = pipe2(addxy, add1, add2, add3);
const mix = pipe2(addxy, String, Number);
console.log(addxyAgain(2, 3)); // -> 5
console.log(addxyAnd1(2, 3)); // -> 6
console.log(addxyAnd1And2(2, 3)); // -> 8
console.log(addAll(2, 3)); // -> 11
console.log(mix(2, 3)); // -> 5