Цепочка методов с asyn c в Typescript (только что нашел решение) - PullRequest
3 голосов
/ 10 июля 2020

Я пишу модуль, направленный на подготовку запроса перед его вызовом в базу данных. Код на vanilla javascript работал довольно хорошо, но когда я попытался написать его на Typescript, я получил ошибку: Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member

Мой код в Javascript:

class QueryBuilder {
  constructor(query) {
    this.query = query;
  }

  sort(keyOrList, direction) {
    this.query = this.query.sort(keyOrList);
    return this;
  }

  skip(value) {
    this.query = this.query.skip(value);
    return this;
  }

  limit(value) {
    this.query = this.query.limit(value);
    return this;
  }

  then(cb) {
    cb(this.query.toArray());
  }
}

Код в машинописном тексте:

class QueryBuilder {
  public query: Cursor;
  constructor(query: Cursor) {
    this.query = query;
  }

  public sort(keyOrList: string | object[] | object, direction: any) {
    this.query = this.query.sort(keyOrList);
    return this;
  }

  public skip(value: number) {
    this.query = this.query.skip(value);
    return this;
  }

  public limit(value: number) {
    this.query = this.query.limit(value);
    return this;
  }

  public then(cb: Function) {
    cb(this.query.toArray());
  }
}

Как я назвал эти методы:

const query = await new QueryBuilder(Model.find())
    .limit(5)
    .skip(5)

Надеюсь, кто-нибудь может мне с этим помочь. Заранее спасибо.

* Обновлено: я расширил класс QueryBuilder из Buitin Promise, а затем заменил метод then на QueryBuilder.prototype.then. Код теперь исполняемый, но я не совсем понял super(executor) в конструктор. Требуется executor(resolve: (value?: T | PromiseLike<T> | undefined) => void, reject: (reason?: any) => void): void, поэтому я просто создал тупого исполнителя. Как это влияет на код?

class QueryBuilder<T> extends Promise<T> {
  public query: Cursor;
  constructor(query: Cursor) {
    super((resolve: any, reject: any) => {
      resolve("ok");
    });
    this.query = query;
  }

  public sort(keyOrList: string | object[] | object, direction?: any) {
    this.query = this.query.sort(keyOrList);
    return this;
  }

  public skip(value: number) {
    this.query = this.query.skip(value);
    return this;
  }

  public limit(value: number) {
    this.query = this.query.limit(value);
    return this;
  }
}

QueryBuilder.prototype.then = function (resolve: any, reject: any) {
  return resolve(this.query.toArray());
};

1 Ответ

2 голосов
/ 10 июля 2020

Проблема

Что делает TypeScript?

Алгоритм TypeScript для оценки типа операнда для await выглядит примерно так (это очень упрощенное объяснение) [ ссылка ] :

  1. Тип является обещанием? Если да, от go до шаг 1 с обещанным типом. Если нет от go до шаг 2 .
  2. Является ли тип применимым? Если нет, верните тип. Если да, выдает ошибку типа: «Тип операнда 'await' должен быть действительным обещанием или не должен содержать вызываемого члена 'then' (ts1320)».

Что TypeScript делает в вашем пример?

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

Операнд для await:

new QueryBuilder(Model.find())
.limit(5)
.skip(5)
  1. Вызов skip не возвращает обещание. Мы от go до шаг 2 (Примечание: ни вызов limit, ни создание экземпляра QueryBuilder).
  2. skip возвращает экземпляр QueryBuilder, который имеет вызываемого члена then. Это приводит к ошибке типа: «Тип операнда 'await' должен быть действительным обещанием или не должен содержать вызываемого члена 'then' (ts1320)».

Определение вашего класса с вызываемым Элемент 'then':

class QueryBuilder {
  public query: Cursor;
  constructor(query: Cursor) {
      this.query = query;
  }
  
  ...
  
  public then(cb: Function) {
      cb(this.query.toArray());
  }
}

Почему возникает ошибка TypeScript?

Теперь мы понимаем, как TypeScript выдал ошибку типа. Но почему он выдает эту ошибку? JavaScript позволяет вам await на что угодно .

[rv] = await expression;

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

- Документация MDN для await

Почему TypeScript говорит: «Тип операнда 'await' [если это недопустимое обещание] не должен содержать вызываемого члена 'then'»? Почему он не позволяет await на затемненном компьютере? MDN даже дает пример, в котором вы await в таблице.

async function f2() {
  const thenable = {
    then: function(resolve, _reject) {
      resolve('resolved!')
    }
  };
  console.log(await thenable); // resolved!
}

f2();

- Пример MDN await в таблице

Исходный код TypeScript хорошо прокомментирован. Он гласит:

Тип не был обещанием, поэтому его нельзя было развернуть дальше. Пока тип не имеет вызываемого свойства «then», возвращать тип безопасно; в противном случае выдается сообщение об ошибке, и мы возвращаем значение undefined.

Примером «thenable» без обещания может быть:

await { then(): void {} }

«thenable» не соответствует минимальному определению для Обещание. Когда обещание, совместимое с Promise / A + или ES6, пытается принять это значение, обещание никогда не будет выполнено. Мы рассматриваем это как ошибку, чтобы помочь выявить ранний индикатор проблемы во время выполнения. Если пользователь хочет вернуть это значение из функции asyn c, ему нужно будет обернуть его другим значением. Если они хотят, чтобы это рассматривалось как обещание, они могут привести к <any>.

- Ссылка

Из прочтения я понял, что TypeScript не выполняет await в таблицах, не являющихся обещаниями, потому что не может гарантировать, что реализация соответствует минимуму SPE c, как определено Promises / A + , таким образом, предполагается, что это ошибка.

Комментарии к ваше решение

В решении, которое вы пробовали и добавили в контекст вашего вопроса, вы определили класс QueryBuilder для расширения собственного обещания, а затем вы переопределили then член. Хотя это, похоже, дает желаемый эффект, есть некоторые проблемы:

Создание экземпляра вашего класса имеет неразумное поведение

В результате расширения класса вам необходимо вызвать его родительский конструктор, прежде чем вы сможете ссылаться контекст this. Тип конструктора родительского класса:

(resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void

И, как вы обнаружили, вам нужно передать что-то , удовлетворяющее этому контракту, только для того, чтобы заставить его работать. . Также после создания экземпляра ваш класс возвращает обещание, разрешенное с произвольным значением. Код, который не имеет значения и является необоснованным, является источником путаницы, неожиданного поведения и может привести к ошибкам.

Вы нарушили контракт типа, определенный интерфейсом обещания

Интерфейс определяет другие методы, такие как catch. Потребитель класса может попытаться использовать метод catch, и хотя контракт позволяет ему это сделать, поведение не такое, как ожидалось. Это ведет к следующему пункту.

Вы не используете TypeScript в своих интересах

Вы упомянули в своем вопросе:

Код в ванили JavaScript работал довольно хорошо, но когда я попытался написать его на TypeScript, я получил ошибку

Типы существуют по определенной причине, одна из которых заключается в том, что они уменьшают вероятность ошибок, обеспечивая выполнение контракта между интерфейсами. Если вы попытаетесь их обойти, это приведет к путанице, неожиданному поведению и повышенному риску ошибок. Зачем вообще использовать TypeScript?

Решение

Теперь, когда мы понимаем, в чем заключается ошибка и почему она возникает, мы можем придумать решение. Решение потребует реализации члена then для соответствия минимальному spe c в Promises / A +, поскольку мы определили, что это является причиной ошибки. Нас интересует только spe c для интерфейса then, а не детали его реализации:

  • 2.2.1 Оба onFulfilled и onRejected являются необязательные аргументы
  • 2.2.7 then должны возвращать обещание

Определение TypeScript для then также полезно для ссылки (примечание: I ' Мы внесли некоторые изменения для удобочитаемости):

/**
 * Attaches callbacks for the resolution and/or rejection of the Promise.
 * @param onfulfilled The callback to execute when the Promise is resolved.
 * @param onrejected The callback to execute when the Promise is rejected.
 * @returns A Promise for the completion of which ever callback is executed.
 */
then<
  TResult1 = T,
  TResult2 = never
>(
  onfulfilled?:
    | ((value: T) => TResult1 | PromiseLike<TResult1>)
    | undefined
    | null,
  onrejected?:
    | ((reason: any) => TResult2 | PromiseLike<TResult2>)
    | undefined
    | null
): Promise<TResult1 | TResult2>;

Сама реализация, скорее всего, будет следовать этому алгоритму:

  1. Выполнить ваш собственный журнал c
  2. Если шаг 1 было успешно разрешено с результатом вашего пользовательского logi c, в противном случае отклоните с ошибкой

Вот демонстрация примера реализации, которая должна помочь вам начать собственная реализация.

class CustomThenable {
  async foo() {
    return await 'something';
  }

  async then(
    onFulfilled?: ((value: string) => any | PromiseLike<string>) | undefined | null,
  ): Promise<string | never> {
    const foo = await this.foo();
    if (onFulfilled) { return await onFulfilled(foo) }
    return foo;
  }
}

async function main() {
  const foo = await new CustomThenable();
  console.log(foo);
  const bar = await new CustomThenable().then((arg) => console.log(arg));
  console.log(bar);
}

main();
...