首页 > 解决方案 > 带有 ArgumentTypes 的 n 参数的 Typescript 类型安全 curry 函数

问题描述

试图创建一个类型安全的柯里化函数。我在 SO 上看到的所有答案都建议函数重载,我不想这样做。

我碰到type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;

它确实提供了函数的所有参数类型,并且可以通过索引选择参数类型。但是,如何对 ArgumentTypes 进行切片以仅具有正确数量的参数及其类型?

在下面的示例中,curried(1, 1);给出错误“预期 3 个参数,但得到 2 个”。

function curry<T extends Function>(fn: T) {
  const fnArgs: readonly string[] = args(fn);
  const cachedArgs: readonly any[] = [];

  type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;

  type FnArguments = ArgumentTypes<typeof fn>;

  function curryReducer(...args: FnArguments) {
    cachedArgs.push(...args);
    return cachedArgs.length >= fnArgs.length
      ? fn(...cachedArgs)
      : curryReducer;
  }

  return curryReducer;
}

function args<T extends Function>(fn: T): readonly string[] {
  const match = fn
    .toString()
    .replace(/[\r\n\s]+/g, ' ')
    .match(/(?:function\s*\w*)?\s*(?:\((.*?)\)|([^\s]+))/);

  return match
    ? match
        .slice(1, 3)
        .join('')
        .split(/\s*,\s*/)
    : [];
}

function adding(a: number, b: number, c: number) {
  return a + b + c;
}

const curried = curry(adding);
const add2 = curried(1, 1);
```;

标签: typescript

解决方案


我不建议使用正则表达式来操作字符串格式的函数 - 相反,您可以使用以下格式(在 javascript 中)创建一个非常简单的 curry 函数:


function curry (fn) {
  return (...args) => {
    if (args.length >= fn.length) {
      return fn(...args);
    }

    return (...more) => curry(fn)(...args, ...more);
  }
}

该函数接受函数 fn,然后返回一个新函数,该函数接受一定数量的参数 (...args)。

然后我们可以检查 args 是否与函数所需的参数数量一样长(这只是 function.length)。这是基本情况,就像这样:

const add = (a, b, c) => a + b + c;
add.length // = 3

curry(add)(1, 2, 3) // = 6

在我们应用的参数少于所需参数的情况下,我们需要返回一个新函数。此函数将接受更多参数(...更多)并将这些附加到原始 ...args,即:

 (...more) => curry(fn)(...args, ...more);

为了使这个与打字稿一起玩,这有点复杂。我建议看一下本教程以更好地理解。

我已经调整了他们的 CurryV5 变体(因为占位符超出了范围)并更新为使用更现代的打字稿功能,因为语法更简单。这应该是获得您所追求的行为的最低要求:


// Drop N entries from array T
type Drop<N extends number, T extends any[], I extends any[] = []> =
    Length<I> extends N
    ? T
    : Drop<N, Tail<T>, Prepend<Head<T>, I>>;

// Add element E to array A (i.e Prepend<0, [1, 2]> = [0, 1, 2])
type Prepend<E, A extends any[]> = [E, ...A];

// Get the tail of the array, i.e Tail<[0, 1, 2]> = [1, 2]
type Tail<A extends any[]> = A extends [any] ? [] : A extends [any, ...infer T] ? T : never;

// Get the head of the array, i.e Head<[0, 1, 2]> = 0
type Head<A extends any[]> = A extends [infer H] ? H : A extends [infer H, ...any] ? H : never;

// Get the length of an array
type Length<T extends any[]> = T['length'];

// Use type X if X is assignable to Y, otherwise Y
type Cast<X, Y> = X extends Y ? X : Y;

// Curry a function
type Curry<P extends any[], R> =
    <T extends any[]>(...args: Cast<T, Partial<P>>) =>
        Drop<Length<T>, P> extends [any, ...any[]]
        ? Curry<Cast<Drop<Length<T>, P>, any[]>, R>
        : R;

function curry<P extends any[], R>(fn: (...args: P) => R) {
    return ((...args: any[]) => {
        if (args.length >= fn.length) {
            return (fn as Function)(...args) as R;
        }

        return ((...more: any[]) => (curry(fn) as Function)(...args, ...more));
    }) as unknown as Curry<P, R>;
}

const add = curry((a: number, b: number, c: number) => a + b + c);

const add2 = add(1, 1);
add2(3); // 5
add2(3, 4); // error - expected 0-1 arguments but got 2
add2('foo'); // error - expected parameter of type number


推荐阅读