非Promiseな値をawaitする

Promise.resolve の使い所 に関連してもう一つ。 awaitPromise じゃない値でも扱える。

上記記事の example より:

example.ts
class {
  private _prop: string

  private get propPromise(): Promise<string> {
    return Promise.resolve(
      this._prop ||
        API.fetch().then(remoteResource => {
          this.prop = remoteResource.prop
          return this.prop
        })
    )
  }

  doSomethingWithProp() {
    const propValue = await this.propPromise
    // using propValue ...
  }
}

たとえば this._prop の値をそのまま返すようにして、
戻り値の型は string | Promise<string> にする:

example.ts
  private get propPromise(): string | Promise<string> {
    return this._prop ||
      API.fetch().then(remoteResource => {
        this.prop = remoteResource.prop
        return this.prop
      })
  }

  doSomethingWithProp() {
    const propValue = await this.propPromise
    // using propValue ...
  }

差分は以下:

- private get propPromise(): Promise<string> {
+ private get propPromise(): string | Promise<string> {
-   return Promise.resolve(
-     this._prop ||
+   return this._prop ||
      API.fetch().then(remoteResource => {
        this.prop = remoteResource.prop
        return this.prop
      })
-   )
  }

使う側が単に await してる限りはこれでも動く。
呼び出した関数スコープで値を付加するために .then したくなったら書き方が変わる。

  createConvertedSomethingPromise(...args): Promise<Hoge> {
    const converter = this.createConverter(...args)
    return this.propPromise.then(prop => converter(prop))
  }

こういうやつ。 <string> 返ったときに .then 生えてないから!ってぶっ壊れるので
Promise.resolvePromise 化する(Promise が渡ってきたときはそのままになる):

  createConvertedSomethingPromise(...args): Promise<Hoge> {
    const converter = this.createConverter(...args)
-   return this.propPromise.then(prop => converter(prop))
+   return Promise.resolve(this.propPromise).then(prop => converter(prop))
  }

あるいは async & await

- createConvertedSomethingPromise(...args): Promise<Hoge> {
+ async createConvertedSomething(...args): Promise<Hoge> {
    const converter = this.createConverter(...args)
-   return this.propPromise.then(prop => converter(prop))
+   const prop = await this.propPromise
+   return converter(prop)
  }

戻り値の型が複数あると、呼び出し先でいちいちそれぞれの型に合わせたハンドリングする必要が出てくる。具体的に、この場合の Promise 化する処理のように、型を変換するコードが呼び出す毎に発生するので、 ワザワザやる必要はない。

話戻って await の挙動について、
後続の値が Promise(というか thenable?) のときは .then のチェーンたどって unwrapして、 not Promise だった場合はそのままスルーしてるのかと思っていたんだけど、どうやらそうじゃないらしい。

... If the value of the expression following the await operator is not a Promise, it's converted to a resolved Promise.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#Description

要は await notPromiseValue については await Promise.resolve(notPromiseValue) に近い挙動を暗黙的に行っているようだ。(これが言いたかっただけ) つまり、後続の値を Promise 化する。自分の想像と逆だった。

25.5.5.3 AsyncFunctionAwait ( value ) ... mean the same thing as:

  1. Let asyncContext be the running execution context.
  2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
  3. Let resolveResult be ! Call(promiseCapability.[[Resolve]], undefined, « value »). ...

https://www.ecma-international.org/ecma-262/8.0/#sec-async-functions-abstract-operations-async-function-await

25.4.1.5 NewPromiseCapability ( C )

  1. If IsConstructor(C) is false, throw a TypeError exception.
  2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 25.4.3.1).
  3. Let promiseCapability be a new PromiseCapability { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }.
  4. Let executor be a new built-in function object as defined in GetCapabilitiesExecutor Functions.
  5. Set executor.[[Capability]] to promiseCapability.
  6. Let promise be ? Construct(C, « executor »).
  7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
  8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
  9. Set promiseCapability.[[Promise]] to promise.
  10. Return promiseCapability.

https://www.ecma-international.org/ecma-262/8.0/#sec-newpromisecapability

斜め読みした感じの解釈だけど、NewPromiseCapabilityPromise 作ったあとに
そこで (作ったPromiseCapability).resolve に await する対象の value を引数にして Call してる

ついでに Promise.resolve はというと

25.4.4.5 Promise.resolve ( x )

  1. Let C be the this value.
  2. If Type(C) is not Object, throw a TypeError exception.
  3. If IsPromise(x) is true, then a. Let xConstructor be ? Get(x, "constructor"). b. If SameValue(xConstructor, C) is true, return x.
  4. Let promiseCapability be ? NewPromiseCapability(C).
  5. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »).
  6. Return promiseCapability.[[Promise]].

https://www.ecma-international.org/ecma-262/8.0/#sec-promise.resolve

戻り値若干違ったりするけど、Promiseをresolveする手続きがほぼ一緒な感じ