Last active
May 5, 2025 02:06
-
-
Save toridoriv/e57e6f64d20525f2b8beeff01a2d8de8 to your computer and use it in GitHub Desktop.
Strongly typed curry and uncurry functions.
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
/** | |
* @module arity | |
* Made using a Node.js v23.11.0 and Typescript v5.8.3. | |
*/ | |
/** | |
* Represents an array of any type. | |
*/ | |
export type AnyArray = Array<any>; | |
/** | |
* Represents an array or a readonly array of any type. | |
*/ | |
export type ArrayOrReadonlyArray<T = any> = Array<T> | ReadonlyArray<T>; | |
/** | |
* Marks a type as writable. | |
* | |
* @example | |
* | |
* ```typescript | |
* type ReadonlyObject = { readonly a: string }; | |
* type WritableObject = Writable<ReadonlyObject>; // { a: string } | |
* | |
* type ReadonlyArr = readonly number[]; | |
* type WritableArray = Writable<ReadonlyArr>; // number[] | |
* ``` | |
* | |
*/ | |
export type Writable<T> = T extends readonly [...infer U] ? U : { -readonly [P in keyof T]: T[P] }; | |
/** | |
* Converts a number to a positive number. | |
* | |
* @example | |
* | |
* ```typescript | |
* type PositiveNumber = ToPositive<5>; // 5 | |
* type NegativeNumber = ToPositive<-5>; // 5 | |
* ``` | |
* | |
*/ | |
export type ToPositive<N extends number> = `${N}` extends `-${infer I extends number}` ? I : N; | |
/** | |
* Creates a tuple where N is the desired length and optionally you can pass it an array of types to be filled. If the | |
* passed array is smaller than expected length, the tuple will be filled with any. | |
* | |
* @example | |
* | |
* ```typescript | |
* type Tuple = BuildTuple<2>; // [any, any] | |
* type AnotherTuple = BuildTuple<3, [number, string]>; // [number, string, any] | |
* ``` | |
* | |
*/ | |
export type BuildTuple<N extends number, List extends AnyArray = []> = List extends { length: ToPositive<N> } | |
? List | |
: BuildTuple<ToPositive<N>, [...List, any]>; | |
/** | |
* Obtains the difference between two numbers. | |
* | |
* @example | |
* | |
* ```typescript | |
* type ValidOperation = Subtract<5, 3>; // 2 | |
* type InvalidOperation = Subtract<3, 5>; // never | |
* type AnotherValidOperation = Subtract<5, -3>; // 2 | |
* ``` | |
* | |
*/ | |
export type Subtract<X extends number, Y extends number> = | |
BuildTuple<X> extends [...infer A, ...BuildTuple<Y>] ? A["length"] : never; | |
/** | |
* Represents a function with any number of arguments. | |
* | |
* @template Args - The type of the arguments. Defaults to {@linkcode AnyArray}. | |
* @template Ret - The type of the return value. Defaults to `any`. | |
*/ | |
export type Callable<Args extends AnyArray = AnyArray, Ret = any> = (...args: Args) => Ret; | |
/** | |
* Compares two numbers and returns true if the first number is larger than the second. | |
* | |
* @example | |
* | |
* ```typescript | |
* type Larger = IsLargerThan<5, 3>; // true | |
* type Smaller = IsLargerThan<3, 5>; // false | |
* type Equals = IsLargerThan<5, 5>; // false | |
* ``` | |
* | |
*/ | |
export type IsLargerThan<X extends number, Y extends number> = Subtract<X, Y> extends never | 0 ? false : true; | |
/** | |
* Removes the first element of an array. | |
* | |
* @example | |
* | |
* ```typescript | |
* type Shifted = Shift<[1, 2, 3]>; // [2, 3] | |
* ``` | |
* | |
*/ | |
export type Shift<T> = T extends [any, ...infer Tail] ? Tail : never; | |
/** | |
* Decrements a number by one. | |
* | |
* @example | |
* | |
* ```typescript | |
* type Decremented = Decrement<5>; // 4 | |
* ``` | |
* | |
*/ | |
export type Decrement<N extends number> = Subtract<N, 1>; | |
/** | |
* Implements {@link Array.slice} as a type. | |
* | |
* @example | |
* | |
* ```typescript | |
* type Animals = ["ant", "bison", "camel", "duck", "elephant"]; | |
* type Example1 = Slice<Animals, 2>; // ["camel", "duck", "elephant"] | |
* type Example2 = Slice<Animals, 2, 4>; // ["camel", "duck"] | |
* type Example3 = Slice<Animals, 1, 5>; // ["bison", "camel", "duck", "elephant"] | |
* type Example4 = Slice<Animals, -2>; // ["duck", "elephant"] | |
* type Example5 = Slice<Animals, 2, -1>; // ["camel", "duck"] | |
* type Example6 = Slice<Animals>; // ["ant", "bison", "camel", "duck", "elephant"] | |
* ``` | |
* | |
*/ | |
export type Slice< | |
A extends ArrayOrReadonlyArray, | |
Start extends number = 0, | |
End extends number = A["length"], | |
> = Slice.RemoveHead<Slice.RemoveTail<Writable<A>, End>, Start>; | |
/** | |
* Commonly used types for {@link Slice}. | |
*/ | |
export namespace Slice { | |
type ParseEnd<N extends number, Len extends number> = | |
IsLargerThan<N, Len> extends true ? 0 : `${N}` extends `-${infer I extends number}` ? I : Subtract<Len, N>; | |
type ParseStart<N extends number, Len extends number> = | |
IsLargerThan<N, Len> extends true ? Len : `${N}` extends `-${infer I extends number}` ? Subtract<Len, I> : N; | |
/** | |
* Removes the last `N` elements from an array type. | |
*/ | |
export type RemoveTail< | |
A extends AnyArray, | |
N extends number, | |
Parsed extends number = ParseEnd<N, A["length"]>, | |
> = Parsed extends 0 ? A : A extends [...infer H, any] ? RemoveTail<H, N, Decrement<Parsed>> : A; | |
/** | |
* Removes the first `N` elements from an array type. | |
*/ | |
export type RemoveHead< | |
A extends AnyArray, | |
N extends number, | |
Parsed extends number = ParseStart<N, A["length"]>, | |
> = Parsed extends 0 ? A : A extends [any, ...infer T] ? RemoveHead<T, N, Decrement<Parsed>> : A; | |
} | |
/** | |
* Represents a curried function. | |
* | |
* @template Fn - The function to be curried. | |
* @template Params - The parameters of the function. | |
*/ | |
export type Curried<Fn extends Callable, Params extends AnyArray = Parameters<Fn>> = ([never] extends Params | |
? (...args: Params) => ReturnType<Fn> | |
: Params extends [] | [any] | |
? (...args: Params) => ReturnType<Fn> | |
: (...args: Slice<Params, 0, 1>) => Curried<Fn, Shift<Params>>) & { original: Fn }; | |
/** | |
* Curries the given function. That is, it transforms a function that takes multiple arguments into a sequence of | |
* functions that each take a single argument. | |
* | |
* @param fn - The function to be curried. | |
* @returns A curried version of the function. | |
* @example | |
* | |
* ```typescript | |
* const add = (a: number, b: number) => a + b; | |
* const curriedAdd = curry(add); | |
* | |
* const add5 = curriedAdd(5); | |
* const result = add5(10); // 15 | |
* | |
* console.assert(result === 15); | |
* ``` | |
* | |
*/ | |
export function curry<Fn extends Callable>(fn: Fn) { | |
try { | |
const base = fn.name.replaceAll("bound ", ""); | |
const name = base.startsWith("curried") ? base : `curried${base[0].toUpperCase()}${base.substring(1)}`; | |
const arity = fn.length; | |
const { [name]: curried } = { | |
[name](...args: AnyArray) { | |
return arity <= 1 ? fn(...args) : curry(fn.bind(null, ...args)); | |
}, | |
}; | |
Object.defineProperty(curried, "original", { value: fn, enumerable: true }); | |
return curried as Curried<Fn>; | |
} catch { | |
if (typeof fn !== "function") { | |
throw new TypeError(`Expected a function, got ${typeof fn}`); | |
} | |
const asStr = fn.toString(); | |
const args = asStr.substring(1, asStr.indexOf(")")).split(","); | |
const body = asStr.substring(asStr.indexOf(")") + 1).replace("=>", "return "); | |
return curry(new Function(...args, body) as Callable); | |
} | |
} | |
/** | |
* Represents an uncurried function. | |
* | |
* @template ForR - A function or the result of a function. | |
* @template Params - The accumulated parameters of the function. | |
*/ | |
export type Uncurried<ForR, Params extends AnyArray = []> = ForR extends (...args: infer A) => infer R | |
? Uncurried<R, [...Params, ...A]> | |
: (...args: Params) => ForR; | |
/** | |
* Uncurries the given function. That is, it transforms a curried function into a function that takes multiple | |
* arguments. | |
* | |
* @param fn - The function to be uncurried. | |
* @returns An uncurried version of the function. | |
*/ | |
export function uncurry<Fn extends Callable>(fn: Fn) { | |
const base = fn.name.replaceAll("bound ", "").replaceAll("curried", "uncurried"); | |
const name = `${base[0].toLowerCase()}${base.substring(1)}`; | |
const { [name]: uncurried } = { | |
[name](...args: AnyArray) { | |
// @ts-ignore: ¯\_(ツ)_/¯ | |
const arity = fn.original?.length || fn.length; | |
if (!arity) return fn(...args); | |
return args.reduce((fn, arg) => fn(arg), fn); | |
}, | |
}; | |
return uncurried as Uncurried<Fn>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment