Общая функция карри с 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>

многие языки имеют развернутые типы, такие как эти запеченные, потому что немногие системы типов поддерживают этот уровень рекурсивного управления потоком при определении типов.