Last active
August 24, 2022 12:39
-
-
Save iki/6472c0775cc0847fc667c01524cafa6b to your computer and use it in GitHub Desktop.
Dynamic function arguments currying in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Dynamic function arguments currying in TypeScript | |
// - For functions with any fixed arguments it keeps currying the function | |
// until all fixed arguments are provided, then returns the result | |
// - For functions with dynamic arguments only it keeps currying the function | |
// as long as the call provides any spices, otherwise returns the result | |
// - Source: https://gist.github.com/iki/6472c0775cc0847fc667c01524cafa6b | |
type Args = readonly unknown[] | |
type Func<A extends Args = any[], R = any> = (...args: A) => R | |
type Slices<A extends Args, I extends Args = []> = A extends readonly [infer F, ...infer R] | |
? [...I, F] | Slices<R, [...I, F]> | |
: A | [] | |
type Spices<F extends Func> = Slices<Parameters<F>> | |
type Rest<F extends Func, S extends Spices<F>> = F extends Func<[...S, ...infer A]> ? A : never | |
type FixedLength<A extends Args> = number extends A['length'] ? never : A | |
type NonEmpty<A extends Args | null = null> = A extends null ? [any, ...any] : A extends [] ? never : A | |
type Curried<F extends Func, S extends Spices<F>> = S extends NonEmpty<FixedLength<Parameters<F>>> | |
? ReturnType<F> // Call fixed arguments function if all were passed | |
: S extends NonEmpty | |
? Curry<Func<Rest<F, S>, ReturnType<F>>> // Return spiced curry if some spices were passed | |
: Parameters<F> extends NonEmpty | |
? F // Return unchanged fixed arguments function if no spices were passed | |
: ReturnType<F> // Call dynamic arguments function if no other spices were passed | |
type Curry<F extends Func> = <S extends Spices<F>>(...spices: S) => Curried<F, S> | |
// Dynamic curry | |
const curry = | |
<F extends Func>(fn: F) => | |
<S extends Spices<F>>(...spices: S): Curried<F, S> => | |
// (console.log(fn.name, fn.length, spices.length, spices) as any) || | |
spices.length >= (fn.length || Infinity) | |
? fn(...spices) // Call fixed arguments function if all were passed | |
: spices.length | |
? curry(spiced(fn, spices)) // Return spiced curry if some spices were passed | |
: fn.length | |
? fn // Return unchanged fixed arguments function if no spices were passed | |
: fn() // Call dynamic arguments function if no other spices were passed | |
// Helper to spice function, keep original name and set correct arguments length | |
const spiced = <S extends Args, A extends Args, R>(fn: Func<[...S, ...A], R>, spices: S) => | |
Object.defineProperties((...args: A) => fn(...spices, ...args), { | |
name: { value: fn.name, writable: false }, | |
length: { value: Math.max(0, fn.length - spices.length), writable: false }, | |
}) | |
// Examples | |
const sum3 = (a: number, b: number, c: number) => a + b + c | |
const dynSum3 = curry(sum3) | |
dynSum3()(1, 2, 3) + 0 // 6 | |
dynSum3(1, 2)()(4) + 0 // 7 | |
dynSum3(2)(3)()(4) + 0 // 9 | |
// dynSum3(2)(3)()(4, 5) + 1 // TS: Expected 1 arguments, but got 2. | |
const sum = (...numbers: number[]) => numbers.reduce((res, n) => res + n, 0) | |
const dynSum = curry(sum) | |
dynSum() + 0 // 0 | |
dynSum(1)() + 0 // 1 | |
dynSum(1)(2, 3)() + 0 // 6 | |
// Mixing fixed and dynamic arguments returns result when all fixed arguments are passed | |
const sum1 = (a: number, ...numbers: number[]) => sum(a, ...numbers) | |
const dynSum1 = curry(sum1) | |
dynSum1()(1) + 0 // 1 | |
dynSum1()(1, 2, 3) + 0 // 6 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment