Zoeken op website

Wanneer je een functie gebruikt als een first-class waarde in TypeScript, is er een manier om generieke bindings door te geven?


Ik probeer TypeScript-functies te wrappen, waarbij nieuwe functies worden teruggegeven die extra gedrag toevoegen. Bijvoorbeeld, in de minimale reproductie hieronder (zie playground) retourneert de functie na het wrappen altijd een Promise en worden de argumenten en de returnwaarde in de console gelogd.

Ik heb echter gemerkt dat als de oorspronkelijke functie enige generieke typen heeft, ik niet weet hoe ik dat generieke type kan binden zodat het beschikbaar is op de ingepakte functie, zelfs als het goed bekend is in de aanroepende context.

Het probleem wordt geïllustreerd door const delayedValue in het onderstaande script. Typescript lijkt te geloven dat dit alleen onbekend kan zijn.

Is er een manier waarop bijvoorbeeld wrappedDelay gedefinieerd kan worden zodat de generieke V-parameter kan doorgaan en daardoor kan aangeven welk retourneertype we van de wrappedDelay-functie kunnen verwachten.

export type AnyFn = (...args: any[]) => any;

/** If sync function - ReturnType. If async function - unwrap Promise of ReturnType */
export type Result<Fn extends AnyFn> = Fn extends () => Promise<infer Promised>
  ? Promised
  : ReturnType<Fn>;

/** Async function with same params and return as Fn, regardless if Fn is sync or async  */
export type WrappedFn<Fn extends AnyFn> = (...parameters:Parameters<Fn>) => Promise<Result<Fn>>

/** Construct wrapped function from function reference */
function wrapFn<Fn extends AnyFn>(fn:Fn) : WrappedFn<Fn>{
    return async (...parameters:Parameters<Fn>) => {
        console.log(`Parameters are ${parameters}`);
        const result = await fn(...parameters);
        console.log(`Result is ${result}`);
        return result;
    }
}

function sum(a:number, b:number){
    return a + b;
}

function delay<V>(value: V, ms:number){
    return new Promise<V>((resolve, reject) => {
        setTimeout(() => resolve(value), ms)
    })
}

const wrappedSum = wrapFn(sum)

const wrappedDelay = wrapFn(delay)

async function example() {
    const sum = await wrappedSum(3,4)
    const delayedValue = await wrappedDelay("hello", 1000)
}  

Antwoord #1

TypeScript heeft geen directe ondersteuning voor "higher kinded types" van het type dat wordt gevraagd in microsoft/TypeScript#1213, dus er is geen algemene manier om generieke functies programmatisch te manipuleren op een manier die een puur op types gebaseerde transformatie van het functietype F naar het functietype G uitdrukt, waarbij alle generieke typeparameters van F worden overgedragen aan G.

Gelukkig is er sinds TypeScript 3.4 ondersteuning voor hogere-orde type-inferentie van generieke functies, waarbij je gedrag zoals dit kunt krijgen voor specifieke functies op het waardenniveau, zoals wrapFn() dat werkt op invoerfuncties die generiek zijn. Dus als f een functie is van type F, en const g=wrapFn(f), is het mogelijk om wrapFn() zo te schrijven dat g van type G is, waarbij alle generieke typeparameters van F zijn overgedragen aan G.

Je kunt microsoft/TypeScript#30215 lezen om te zien hoe dit hogere-orde type-inferentie werkt en welke regels je moet volgen om dit gedrag te krijgen. In het bijzonder verwacht deze functie dat je aparte typeparameters hebt voor de functieargumenten (bijv. A extends any[]) en voor de return (bijv. R). Het werkt niet met een generiek functietype zoals F extends (...args: any[])=>any, waarbij je conditionele hulpfunctietypes gebruikt zoals Parameters<F> of ReturnType<F> om argumenttypes of returntypes eruit te halen.

Dus, als je WrappedFn en wrapFn wijzigt naar het volgende:

type WrappedFn<A extends any[], R> = (...parameters: A) => Promise<R>

function wrapFn<A extends any[], R>(fn: (...args: A) => R): WrappedFn<A, R> {
    return async (...parameters: A) => {
        console.log(`Parameters are ${parameters}`);
        const result = await fn(...parameters);
        console.log(`Result is ${result}`);
        return result;
    }
}

Dan gedragen dingen zich zoals je verwacht, althans voor de voorbeeldcode in je vraag:

const wrappedDelayValue = wrapFn(delay);
// const wrappedDelayValue: <V>(value: V, ms: number) => Promise<Promise<V>>

const delayedValue = await wrappedDelayValue("hello", 1000);
// const delayedValue: string    
console.log(delayedValue.toUpperCase()) // no compiler error now

Speeltuinlink naar code

Gerelateerde artikelen