TypeScript 3.0引入了generic rest parameters。
到目前为止,必须在finite number of function overloads和一系列条件语句中查询curry
函数在TypeScript中,该条件语句查询实现中传递的参数数量。
我希望通用的rest参数最终能够提供实现完全通用的解决方案所需的机制。
我想知道如何使用此新语言功能来编写通用的curry
函数...假设当然可以!
我使用了solution I found on hackernoon进行了一些修改的使用剩余参数的JS实现,如下所示:
function curry(fn) {
return (...args) => {
if (args.length === 0) {
throw new Error("Empty invocation")
} else if (args.length < fn.length) {
return curry(fn.bind(null, ...args))
} else {
return fn(...args)
}
}
}
使用通用的休息参数和函数重载,我尝试在TypeScript中注释此curry
函数的过程如下:
interface CurriedFunction<T extends any[], R> {
(...args: T): void // Function that throws error when zero args are passed
(...args: T): CurriedFunction<T, R> // Partially applied function
(...args: T): R // Fully applied function
}
function curry<T extends any[], R>(
fn: CurriedFunction<T, R>
): CurriedFunction<T, R> {
return (...args: T) => {
if (args.length === 0) {
throw new Error("Empty invocation")
} else if (args.length < fn.length) {
return curry(fn.bind(null, ...args))
} else {
return fn(...args)
}
}
}
但是TypeScript会引发错误:
Type 'CurriedFunction<any[], {}>' is not assignable to type 'CurriedFunction<T, R>'.
Type '{}' is not assignable to type 'R'.
我不知道R
被推断为{}
的位置和原因?
任何来自TypeScript诸神的帮助都将不胜感激。
答案 0 :(得分:4)
目前,正确键入此字符的最大障碍是TypeScript从TypeScript 3.0开始无法连接或拆分元组。有suggestions可以做到这一点,对于TypeScript 3.1及更高版本,可能正在做一些工作,但现在还不存在。到目前为止,您所能做的就是枚举最大长度有限的案例,或者尝试使用trick the compiler into using recursion,即not recommended。
如果我们想象有一个TupleSplit<T extends any[], L extends number>
类型的函数,该函数可以采用一个元组和一个长度并将该长度的元组拆分为初始分量和其余分量,这样TupleSplit<[string, number, boolean], 2>
将产生{ {1}},那么您可以将{init: [string, number], rest: [boolean]}
函数的类型声明为如下形式:
curry
为了能够尝试,我们介绍一个declare function curry<A extends any[], R>(
f: (...args: A) => R
): <L extends TupleSplit<A, number>['init']>(
...args: L
) => 0 extends L['length'] ?
never :
((...args: TupleSplit<A, L['length']>['rest']) => R) extends infer F ?
F extends () => any ? R : F : never;
版本,该版本仅适用于TupleSplit<T, L>
到L
(您可以根据需要添加)。看起来像这样:
3
现在我们可以在类似的函数上测试type TupleSplit<T extends any[], L extends number, F = (...a: T) => void> = [
{ init: [], rest: T },
F extends ((a: infer A, ...z: infer Z) => void) ?
{ init: [A], rest: Z } : never,
F extends ((a: infer A, b: infer B, ...z: infer Z) => void) ?
{ init: [A, B], rest: Z } : never,
F extends ((a: infer A, b: infer B, c: infer C, ...z: infer Z) => void) ?
{ init: [A, B, C], rest: Z } : never,
// etc etc for tuples of length 4 and greater
...{ init: T, rest: [] }[]
][L];
的声明
curry
这些类型对我来说都是正确的。
当然,这并不意味着function add(x: number, y: number) {
return x + y;
}
const curriedAdd = curry(add);
const addTwo = curriedAdd(2); // (y: number) => number;
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never
的实现会对这种类型感到满意。您也许可以像这样实现它:
curry
可能。我还没有测试过。
无论如何,希望这有意义并能给您一些指导。祝你好运!
如果您没有传递所有参数,我不会注意return <L extends TupleSplit<A, number>['init']>(...args: TupleSplit<A, L['length']>['rest']) => {
if (args.length === 0) {
throw new Error("Empty invocation")
} else if (args.length < f.length) {
return curry(f.bind(null, ...args))
} else {
return f(...args as A)
}
}
返回进一步的咖喱函数这一事实。这样做需要递归类型,例如:
curry()
这更像是原始定义。
但是我也注意到,如果您这样做:
type Curried<A extends any[], R> =
<L extends TupleSplit<A, number>['init']>(...args: L) =>
0 extends L['length'] ? never :
0 extends TupleSplit<A, L['length']>['rest']['length'] ? R :
Curried<TupleSplit<A,L['length']>['rest'], R>;
declare function curry<A extends any[], R>(f: (...args: A)=>R): Curried<A, R>;
function add(x: number, y: number) {
return x + y;
}
const curriedAdd = curry(add);
const addTwo = curriedAdd(2); // Curried<[number], number>
const three = addTwo(1); // number
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never
不返回错误,而是返回const wat = curriedAdd("no error?"); // never
。在我看来,这似乎是编译器错误,但我还没有跟进。编辑:好的,我为此提起了Microsoft/TypeScript#26491。
干杯!
答案 1 :(得分:1)
这里最大的问题是,您正在尝试定义具有可变数量的“咖喱味级别”的泛型函数,例如a => b => c => d
或x => y => z
或(k, l) => (m, n) => o
,其中所有这些功能都以相同的(尽管是通用的)类型定义F<T, R>
表示-这在TypeScript中是不可能的因为您不能随意将generic rests
分成两个较小的元组...
从概念上讲,您需要:
FN<A extends any[], R> = (...a: A) => R | (...p: A.Prefix) => FN<A.Suffix, R>
TypeScript AFAIK无法做到这一点。
您最好的选择是使用一些可爱的重载:
FN1<A, R> = (a: A) => R
FN2<A, B, R> = ((a: A, b: B) => R) | ((a: A) => FN1<B, R>)
FN3<A, B, C, R> = ((a: A, b: B, c: C) => R) | ((a: A, b: B) => FN1<C, R>) | ((a: A) => FN2<B, C, R>)
FN4<A, B, C, D, R> = ((a: A, b: B, c: C, d: D) => R) | ((a: A, b: B, c: C) => FN1<D, R>) | ((a: A, b: B) => FN2<C, D, R>) | ((a: A) => FN3<B, C, D, R>)
function curry<A, R>(fn: (A) => R): FN1<A, R>
function curry<A, B, R>(fn: (A, B) => R): FN2<A, B, R>
function curry<A, B, C, R>(fn: (A, B, C) => R): FN3<A, B, C, R>
function curry<A, B, C, D, R>(fn: (A, B, C, D) => R): FN4<A, B, C, D, R>
很多语言都具有像此类嵌入式语言那样的展开类型,因为在定义类型时很少有类型系统支持这种级别的递归流控制。
答案 2 :(得分:1)
使用当前版本的打字稿,可以创建一个相对简单且正确键入的通用库里函数。
class MyFormRequest extends FormRequest {
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules() {
return [
"title" => "required",
"content" => "required",
];
}
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() {
return true;
}
}
这基于两个类型的构造函数:
type CurryFirst<T> = T extends (x: infer U, ...rest: any) => any ? U : never;
type CurryRest<T> =
T extends (x: infer U) => infer V ? U :
T extends (x: infer U, ...rest: infer V) => infer W ? Curried<(...args: V) => W> :
never
type Curried<T extends (...args: any) => any> = (x: CurryFirst<T>) => CurryRest<T>
const curry = <T extends (...args: any) => any>(fn: T): Curried<T> => {
if (!fn.length) { return fn(); }
return (arg: CurryFirst<T>): CurryRest<T> => {
return curry(fn.bind(null, arg) as any) as any;
};
}
describe("Curry", () => {
it("Works", () => {
const add = (x: number, y: number, z: number) => x + y + z;
const result = curry(add)(1)(2)(3)
result.should.equal(6);
});
});
将给定一个函数,返回该函数的第一个参数的类型。CurryFirst
将返回应用了第一个参数的咖喱函数的返回类型。特殊情况是,当函数类型CurryRest
仅接受一个参数时,T
将仅返回函数类型CurryRest<T>
的返回类型基于这两个,T
类型的函数的咖喱版本的类型签名将变为:
T
我在这里做了一些简单的约束:
Curried<T> = (arg: CurryFirst<T>) => CurryRest<T>
指针。这对我来说也没有意义,因为我们在这里输入纯FP土地。如果curry函数将参数累积在一个数组中并执行一个this
而不是多个fn.apply
调用,则可以改善性能。但是必须注意确保可以多次正确调用部分应用的函数。