TypeScript: как написать функцию asyncPipe для композиции асинхронных функций? - PullRequest
3 голосов
/ 09 февраля 2020

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

const getUserById = id => new Promise((resolve, reject) => id === 1
  ? resolve({ id, displayName: 'Jan' })
  : reject('User not found.')
);
const getName = ({ displayName }) => displayName;
const countLetters = str => str.length;
const asyncIsEven = n => Promise.resolve(n % 2 === 0);

const asyncPipe = (...fns) => x => fns.reduce(async (y, f) => f(await y), x);

const userHasEvenName = asyncPipe(
    getUserById,
    getName,
    countLetters,
    asyncIsEven
);

userHasEvenName(1).then(console.log);
// ↳ false
userHasEvenName(2).catch(console.log);
// ↳ 'User not found.'

Здесь asyncPipe составляет обычные функции, а также обещания в антиматематическом порядке (слева направо). Я хотел бы написать asyncPipe в TypeScript, который знает о типах ввода и вывода. Поэтому userHasEvenName должен знать, что он принимает число и возвращает Promise<boolean>. Или, если вы закомментируете getUserById и asyncIsEven, он должен знать, что он принимает User и возвращает число.

Вот вспомогательные функции в TypeScript:

interface User {
    id: number;
    displayName: string;
}

const getUserById = (id: number) => new Promise<User>((resolve, reject) => id === 1
    ? resolve({ id, displayName: 'Jan' })
    : reject('User not found.')
);
const getName = ({ displayName }: { displayName: string }) => displayName;
const countLetters = (str: string) => str.length;
const asyncIsEven = (n: number) => Promise.resolve(n % 2 === 0);

Я хотел бы показать вам все мои подходы к asyncPipe, но большинство из них были далеко. Я обнаружил, что для написания compose функции в TypeScript вам необходимо сильно перегрузить , потому что TypeScript не может обрабатывать обратный вывод и compose выполняется в математическом порядке. Так как asyncPipe сочиняет слева направо, создается впечатление, что его можно написать. Я смог явно написать pipe2, который может составлять две обычные функции:

function pipe2<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
    return x => g(f(x));
}

Как бы вы написали asyncPipe, который асинхронно составляет произвольное количество функций или обещаний и правильно выводит тип возврата?

1 Ответ

4 голосов
/ 09 февраля 2020

Вариант 1: Простой asyncPipe ( детская площадка ):

type MaybePromise<T> = Promise<T> | T

function asyncPipe<A, B>(ab: (a: A) => MaybePromise<B>): (a: MaybePromise<A>) => Promise<B>
function asyncPipe<A, B, C>(ab: (a: A) => MaybePromise<B>, bc: (b: B) => MaybePromise<C>): (a: MaybePromise<A>) => Promise<C>
// extend to a reasonable amount of arguments

function asyncPipe(...fns: Function[]) {
    return (x: any) => fns.reduce(async (y, fn) => fn(await y), x)
}

Пример:

const userHasEvenName = asyncPipe(getUserById, getName, countLetters, asyncIsEven);
// returns (a: MaybePromise<number>) => Promise<boolean>

Предостережение: Это всегда будет возвращать обещание, даже если все аргументы функции syn c.


Вариант 2: Гибрид asyncPipe ( детская площадка )

Давайте попробуем сделать результат Promise, если любая из функций asyn c, в противном случае возвращает результат syn c. Типы здесь раздуваются очень быстро, поэтому я просто использовал версию с одной перегрузкой (два аргумента функции).

function asyncPipe<A, B, C>(ab: (a: A) => B, bc: (b: Sync<B>) => C): < D extends A | Promise<A>>(a: D) => RelayPromise<B, C, D, C>
// extend to a reasonable amount of arguments

function asyncPipe(...fns: Function[]) {
    return (x: any) => fns.reduce((y, fn) => {
        return y instanceof Promise ? y.then(yr => fn(yr)) : fn(y)
    }, x)
}

Я определил двух помощников: Sync всегда даст вам resolved Тип обещания, RelayPromise преобразует последний параметр типа в обещание, если какой-либо из других параметров является обещанием (дополнительную информацию см. На игровой площадке).

Пример:

const t2 = asyncPipe(getName, countLetters)(Promise.resolve({ displayName: "kldjaf" }))
// t2: Promise<number>

const t3 = asyncPipe(getName, countLetters)({ displayName: "kldjaf" })
// t3: number

Предостережение: если вам нужны оба syn c + asyn c в одном типе, это будет очень сложно, и вы должны тщательно его протестировать (в моем примере еще может быть несколько,, я пока использовал только простую версию) ).

Также, возможно, существует причина совместимости, почему fp-ts использует специальную версию pipe, которая улучшает использование TypeScript слева направо Вывод параметров типа (это может быть важно и для вас).


Примечания

Наконец, вы должны решить, стоит ли иметь специальную версию asyncPipe только для Обещания - больше видов и Реализация означает больше потенциальных ошибок.

В качестве альтернативы используйте простой pipe с функторами или монадами в стиле функционального программирования. Например, вместо использования обещания вы можете переключиться на типы Task или TaskEither (см. Пример fp-ts).

...