Обобщение TypeScript не применяется к предыдущей функции, когда она выполняется, как? - PullRequest
4 голосов
/ 12 февраля 2020

У меня есть карри версия map, которая обрабатывает обещания. Требуется два аргумента, по одному за раз. Требуется два параметра типа, чтобы позволить вызывающей стороне предоставлять куски, которые не могут быть выведены. Однако на практике вызывающая сторона всегда должна предоставлять их, потому что типы не выводятся в функции преобразования, которую они передают. Функция выглядит следующим образом:

/**
 * @description
 *   Like `ramda.map`, but handles an iterator that returns a promise (or not).
 *
 * @example
 *   await mapP((x: number) => Promise.resolve(x + 1))([1, 2, 3]) // -> [2, 3, 4]
 */
export const mapP = <T, R>(xf: (value: T) => Promise<R> | R) => (
  data: T[],
): Promise<R[]> => pipe(when(isNil, always([])), map(xf), allP)(data)

Вот я ее вызываю, и вы можете видеть, что x неизвестно системе типов.

Как мне исправить способ написания функции, чтобы типы работали (не отказываясь от карри - я знаю, что это будет понятно, если не карри)?

1 Ответ

6 голосов
/ 12 февраля 2020

Я собираюсь использовать следующее объявление:

declare const mapP: <T, R>(xf: (value: T) => Promise<R> | R) => (
  data: T[],
) => Promise<R[]>;

, которое совпадает с вашей версией, но не беспокоится о реализации. В любом случае, вы столкнулись с проблемой, из-за которой следующее не может определить тип x в обратном вызове:

const res = mapP(x => x.name)(data); // error!
// x is unknown ----> ~

Это не полностью , поэтому не следует ожидать, что компилятор может сделайте вывод, что x должен иметь тип Foo, потому что data имеет тип Foo[]. Существует такая вещь, как контекстная типизация , при которой компилятор выводит типы «назад во времени», наблюдая за тем, как что-то используется, и выясняя, какой тип должен был быть объявлен как чтобы это работало. К сожалению, это слишком сложно для компилятора, чтобы сделать это в обратном направлении через несколько вызовов функций. Ну хорошо.


Большая привлекательность карри, на мой взгляд, заключается в возможности частично применить функцию и затем использовать эту частично примененную функцию позже , например так:

const f = mapP(x => x.name); // error!
// x is unknown --> ~

// later

const res2 = f(data);

В этом случае было бы неправдоподобно ожидать, что компилятор может знать что-нибудь полезное о x, особенно в свете других возможных вызовов, таких как:

const res3 = f([{ name: false }]);

где x должно быть {name: boolean} вместо Foo. Если ваше намерение в x => x.name состоит в том, чтобы x было Foo, вам нужно сообщить это намерение компилятору с помощью аннотации типа:

const res4 = mapP((x: Foo) => x.name)(data); // okay Promise<string[]>

Это решение, которое я ' буду рекомендовать ваш вопрос, как указано; вам не требуется, чтобы разработчик вручную указывал T и R при вызове mapP(). Вместо этого вы аннотируете параметр обратного вызова, чтобы компилятор мог определить T и R сам, что он и делает.

Обратите внимание, что вы можете даже стать более модным и общаться " Я хотел бы, чтобы обратный вызов применялся к что угодно со свойством name и возвращал значение этого типа ", используя обратный вызов generi c:

const g = mapP(<T>(x: { name: T }) => x.name);

const res5 = g(data); // Promise<string[]>;
const res6 = g([{ name: false }]); // Promise<boolean[]>;

И здесь компилятор может использовать некоторые из своих выводов более высокого порядка , представленных в TS3.4, чтобы увидеть, что g() сама является обобщенной c функцией.


Итак, резервное копирование: если ваш вариант использования действительно заключается в немедленном использовании частично примененной функции, вызывая ее без ссылки на нее, вы, вероятно, должны делать это без каррирования. Подход «лучший из обоих миров» может быть перегруженной гибридной функцией, которая одновременно и карри, и не карри:

function mapQ<T, R>(xf: (value: T) => Promise<R> | R, data: T[]): Promise<R[]>;
function mapQ<T, R>(xf: (value: T) => Promise<R> | R): (data: T[]) => Promise<R[]>;
function mapQ<T, R>(
  xf: (value: T) => Promise<R> | R,
  data?: T[]
): ((data: T[]) => Promise<R[]>) | Promise<R[]> {
  return data ? mapQ(xf)(data) : mapQ(xf);
}

Тогда вы можете использовать ее карри в том и только в том случае, если ваше намерение - использовать функцию позже :

const res7 = mapQ(x => x.name, data); // okay Promise<string[]>
const h = mapQ((x: Foo) => x.name);
const res8 = h(data); // okay Promise<string[]>

Хорошо, надеюсь, это поможет; удачи!

Детская площадка ссылка на код

...