Skip to content

Instantly share code, notes, and snippets.

@toridoriv
Last active May 5, 2025 02:06
Show Gist options
  • Save toridoriv/e57e6f64d20525f2b8beeff01a2d8de8 to your computer and use it in GitHub Desktop.
Save toridoriv/e57e6f64d20525f2b8beeff01a2d8de8 to your computer and use it in GitHub Desktop.
Strongly typed curry and uncurry functions.
/**
* @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