Skip to content

Instantly share code, notes, and snippets.

@iki
Last active August 24, 2022 12:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iki/6472c0775cc0847fc667c01524cafa6b to your computer and use it in GitHub Desktop.
Save iki/6472c0775cc0847fc667c01524cafa6b to your computer and use it in GitHub Desktop.
Dynamic function arguments currying in TypeScript
// 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