Общая функция карри с TypeScript 3
введен TypeScript 3.0 общие параметры покоя.
до этого момента, curry
функции должны были быть аннотированы в TypeScript с конечное число перегрузок функция и ряд условных операторов, запрашивающих количество переданных аргументов в реализации.
Я надеюсь, что общие параметры rest, наконец, предлагает механизм, необходимый для реализации полностью универсального решение.
Я хотел бы знать, как использовать эту новую функцию языка, чтобы написать общий ...если это возможно, конечно!
реализация JS с использованием параметров rest, которые я немного изменил из решение, которое я нашел на hackernoon выглядит так:
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)
}
}
}
используя общие параметры rest и перегрузки функций, моя попытка аннотировать это curry
функция в TypeScript выглядит так это:
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
выводится как {}
?
любая помощь от богов машинописного текста среди вас была бы очень признательна.
2 ответов
прямо сейчас самым большим препятствием для правильного ввода этого является неспособность TypeScript объединять или разделять кортежи с TypeScript 3.0. Есть предложения для этого, и что-то может быть в работах для TypeScript 3.1 и за его пределами, но сейчас этого просто нет. На сегодняшний день все, что вы можете сделать, это перечислить случаи до некоторой максимальной конечной длины или попытаться обмануть компилятор с помощью рекурсии что это не рекомендовано.
если мы представим, что там было TupleSplit<T extends any[], L extends number>
функция типа, которая могла бы взять кортеж и длину и разделить Кортеж на этой длине на начальный компонент и остальные, так что TupleSplit<[string, number, boolean], 2>
будет производить {init: [string, number], rest: [boolean]}
, тогда вы могли бы объявить свой функции как что-то вроде этого:
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
эти типы выглядят правильными для меня.
конечно, это не значит реализация of 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()
возвращает дополнительные функции curried, если вы не передаете все аргументы. Для этого требуется рекурсивный тип, например:
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
что вместо того, чтобы получить ошибку, он возвращает never
. Этот похоже на ошибку компилятора, но я еще не следил за ней. EDIT: хорошо, я подал Microsoft / TypeScript#26491 об этом.
Ура!
самая большая проблема здесь заключается в том, что вы пытаетесь определить общую функцию с переменным числом "уровней карри" - например 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>
многие языки имеют развернутые типы, такие как эти запеченные, потому что немногие системы типов поддерживают этот уровень рекурсивного управления потоком при определении типов.