Fånga generisk typ av passerad funktion för överbelastningar i TypeScript
Jag tittade på koden i streaming-iterables-paketet, mönstret med att definiera en _fn och sedan överbelasta den med en fn inklusive en curryad version verkar vara ett fall av DRY, det händer i alla funktioner som paketet exponerar.
Nedanstående kod var mitt försök till en generell currying-överlagringsimplementation för paketet:
type Leading<T extends any[]> = T extends [...infer I, infer _] ? I : never;
type Last<T extends any[]> = T extends [...infer _, infer I] ? I : never;
function curried<F extends (...args) => any>(
fn: F
): {
(...args: Parameters<F>): ReturnType<F>;
(...args: Leading<Parameters<F>>): (curried: Last<Parameters<F>>) => ReturnType<F>;
} {
return (...args) =>
args.length == fn.length - 1
? curried => fn(...[...args, curried])
: fn(...args);
}
Det verkar fungera bra tills generics kommer i spel:
function a<T>(b: string, c: number, d: T[]) {
return [b, c, ...d];
}
const f = curried(a);
f('hi', 42, [1, 2, 3]) // d: unknown[], expected: number[]
f('hi', 42) // curried: unknown[], expected: T[]
Jag är medveten om bristen på stöd för högre ordningens generics i TS för närvarande. Men det finns tydligen lösningar? Kan dessa lösningar användas i denna implementation?
Svar #1
Jag ser inget sätt att använda en lösning som specificerar generiska funktioner för att hjälpa dig här. Den lösningen tar en generisk anropssignatur och omvandlar den till en generisk typ... men du behöver omvandla en generisk anropssignatur till en annan generisk anropssignatur, vilket detta inte gör. Och i vilket fall fungerar lösningen bara för enskilda generiska anropssignaturer, inte för vilken som helst som skickas till curried(). Så jag kommer att sluta överväga detta.
Det finns viss support för att härleda generiska funktioner men det fungerar bara i mycket specifika omständigheter och är därför bräckligt. Närmare bestämt fungerar det inte när man skapar överlagrade funktioner, som du gör med resultatet av curried(). Se microsoft/TypeScript#33594 för mer information. Om du behöver att överlagringar ska komma från curried(), är det för närvarande inte möjligt.
Om vi slappnar av kravet på överbelastningar och endast producerar den output som tar bort en parameter från slutet av funktionen, kan du skriva anropssignaturen för curried() så här:
declare function curried<I extends any[], L, R>(fn: (...args: [...I, L]) => R):
((...args: I) => (last: L) => R)
Du kan verifiera att det fungerar så här:
function z<T>(a: T, b: T, c: T) { return [a, b, c]; }
const cZ = curried(z);
// const cZ: <T>(a: T, b: T) => (last: T) => T[]
const cz2 = cZ(5, 6);
// const cz2: (last: number) => number[]
Generisk in, generisk ut. z-funktionen är generisk med typparametern T, och därför är cZ också generisk, och kan anropas för att specificera T. Hurra!
Observera att jag inte använde din a-funktion för att demonstrera. Den "fungerar" fortfarande, men det är inte underbart. Låt oss prova det:
function a<T>(b: string, c: number, d: T[]) { return [b, c, ...d]; }
const f = curried(a);
// const f: <T>(b: string, c: number) => (last: T[]) => (string | number | T)[]
const f2 = f('hi', 42);
// const f2: (last: unknown[]) => unknown[]
Utdata f är generisk, visst. Men det finns ingen bra inferensplats för typparametern T. När du anropar f('hi', 42) måste kompilatorn härleda T direkt, men vad skulle T kunna vara? Det beror inte på 'hi' eller 42. Kompilatorn kan helt enkelt inte veta, så den faller ändå tillbaka på unknown.
Det enda sättet runt det är att specificera T själv när du kallar f, vilket gör dess generiska natur lite mindre imponerande:
const f3 = f<number>('hi', 42)
// const f3: (last: number[]) => (string | number)[]
Det som kan hävdas vara den "rätta" generiska typen för resultatet av curried(a) borde vara
declare const f: (b: string, c: number) => <T>(last: T[]) => (string | number | T)[]
där jag har flyttat T från det yttre funktionsområdet till det returnerade funktionsområdet. Om du hade det, skulle ditt anrop till f() producera en annan generisk funktion:
const f2 = f('hi', 42);
// const f2: <T>(last: T[]) => (string | number | T)[]
Och så skulle du inte ha unknown längre:
const result = f2([1, 2, 3]);
// const result: (string | number)[]
Men det finns helt enkelt inget uppenbart sätt för kompilatorn att veta att den ibland ska flytta den generiska typparameterns omfång från utdatafunktionen till utdatafunktionens utdatafunktion. Och det finns egentligen inget sätt för en utvecklare att säga åt kompilatorn att göra det, eftersom TypeScript saknar uttrycksfullhet för att prata om operationer på generiska anropssignaturer.
Det skulle kräva antingen högre ordningens typer av den sort som efterfrågades i microsoft/TypeScript#1213 eller generiska värden av den sort som efterfrågades i microsoft/TypeScript#17574... eller kanske till och med existentiellt kvantifierade generiska typer av den sort som efterfrågades i microsoft/TypeScript#14466. Eller kanske alla tre. Eller något annat. Just nu, dock, verkar det ligga utom räckhåll.
Så där har du det. Om du släpper på dina krav och tåler vissa specialfall, kan du komma nära det du vill ha. Om du inte kan, antar jag att vi kanske måste vänta tills någon framtida version av TypeScript kommer med stöd för mer kraftfull hantering av generiska funktionstyper.
Playground-länk till kod
