Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type safe curry function #561

Open
somnicattus opened this issue Mar 7, 2024 · 5 comments
Open

Add type safe curry function #561

somnicattus opened this issue Mar 7, 2024 · 5 comments
Labels
feature request New feature or request typing Something type related

Comments

@somnicattus
Copy link
Contributor

somnicattus commented Mar 7, 2024

Related to #560.

I finally found that curry is enough for most users. This worked well with generic-typed functions.

export type Curried<
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    F extends (...args: readonly any[]) => unknown,
> =
    Parameters<F> extends readonly [infer T, ...infer R extends unknown[]]
        ? (...args: R) => (arg: T) => ReturnType<F>
        : never;

export type NthCurried<
    N extends number,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    F extends (...args: readonly any[]) => unknown,
    A extends unknown[] = [unknown],
> =
    Parameters<F> extends readonly [unknown, ...unknown[]]
        ? A['length'] extends N
            ? Curried<F>
            : NthCurried<N, Curried<F>, [unknown, ...A]>
        : never;

/**
 * Curry a function's parameter.
 *
 * @example
 * const pow = curry(Math.pow);
 * const result1 = Math.pow(2, 3); // 8
 * const result2 = pow(3)(2); // 8
 *
 * @example
 * const map = <I, O>(
 *     ...args: Parameters<Curried<typeof _map<I, O>>>
 * ): ReturnType<Curried<typeof _map<I, O>>> => curry(_map<I, O>)(...args);
 * const result1 = _map([1, 2], x => x * 2); // [2, 4]
 * const result2 = map((x: number) => x * 2)([1, 2]); // [2, 4]
 */
export function curry<
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    F extends (...args: readonly any[]) => unknown,
    N extends number | undefined,
>(fn: F, nth?: N): NthCurried<N extends number ? N : 1, F> {
    const curried = ((...args) =>
        (a0: Parameters<F>[0]) =>
            fn(a0, ...args)) as Curried<F>;

    return (nth === undefined || nth === 1 ? curried : curry(curried, nth - 1)) as NthCurried<
        N extends number ? N : 1,
        F
    >;
}
@eranhirsch
Copy link
Collaborator

eranhirsch commented Mar 8, 2024

This still requires the user to use a lot of manual typing that can't be inferred. I tried using it with this example and got typing issues.

const mapped = pipe(
  ['a', 'aa', 'aaa'],
  curry(map)((x) => x.length),
);

The main use for currying is to get inference from Typescript automatically in pipes and as functors. If typing needs to be done manually, it just doesn't provide a lot of additional value over the existing solution.

I do think there's room for a curry function that could be used in pipes to adapt functions that are not dataLast to the pipe (like my example above), but currently the best solution for that would be:

const mapped = pipe(
  ['a', 'aa', 'aaa'],
  ($) => map($, (x) => x.length),
);

which is typed correctly and doesn't require any manual typing.

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzmYYCmcC+cBmUIhwBEUaIaAJgIZEDcAUPTAJ7pwDCArlFMJQApUoVcjDRQAzgB4AYnDQAPMQDsKEuAAoAdDqEBzCQC44pKhQjKANszhVlzANoBdAJRwAvAD44nZQGtlCAB3ZW93ejg4QWFRcWkZb0UVNRM0MwtrOAdfAODlABo4HS1gZRxxOAAleSU0VXUcwJDnJwjIuAB+KrbI42U0ADdxBiZWDC4ePgpKtBhuZQAVMdka5PVtXSgDY1NzKxsHYrtHJ1cPb0a8sLbokVm42UTa+tT0-azS8qg4BcLLkMKxX+yhaPU6mgGVEsnDQxgWbi8VVm8yW6EeYL6g2GjAAxhYJPAcdwoDZwpEVkk6ikNlp9EZXntMsdnAiLv4mqENGCcMpjDJ8m0XMYNDS6cYJrwBEI7mJJI9WRxiVMZnMoItlglWW1RVsJFr2poNKUwJwYMZbrE5QkHAAGM6InlG5QmmCAzYGFxuKjqCXK5Fq1FoR4jChoHGWIQYHC+HEwYAWOAgKhgKS-KqeI1iEA7NKMmwLZyFJNgdBQYXALNwhWVIVVZwjPHKAmJ5PoCgARg8yFQaC5kQA9P3IgA9DptBxEGiFSdT4hUedEJwCyJEnjMDTFlwihQKhRaSx1PQwAAWLmXcEHBqvcFH9BcDfx8GLbYATF2UOg+xehzex5EJ7OM5ENO840Eu2oACQKsWGgQYUGg7uccB7geyhHqe56Xte163ve9BAA

@somnicattus
Copy link
Contributor Author

somnicattus commented Mar 8, 2024

Sorry, it was broken codes...

I finally made it with nth support!

Try here

@eranhirsch eranhirsch added feature request New feature or request typing Something type related labels Mar 29, 2024
@cjquines
Copy link
Collaborator

cjquines commented Apr 1, 2024

hm, that still doesn't deal with the previous example

const mapped = pipe(
  ['a', 'aa', 'aaa'],
  curry(map)((x) => x.length),
);

@somnicattus
Copy link
Contributor Author

somnicattus commented Apr 6, 2024

@cjquines It requires type parameters for generic functions...

const mapped = pipe(
  ['a', 'aa', 'aaa'],
  curry(map<string, number>)((x) => x.length),
);

@cjquines
Copy link
Collaborator

cjquines commented Apr 7, 2024

ah... i think this goes against the remeda philosophy though, which is to not have type annotations if possible...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request typing Something type related
Projects
None yet
Development

No branches or pull requests

3 participants