Closed17

Promiseのインタフェース見るなど

Yug (やぐ)Yug (やぐ)

インタフェース以外もいろいろ調べたり試したりする

まずはインタフェース

interface PromiseLike<T> {
    /**
     * 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): PromiseLike<TResult1 | TResult2>;
}
/**
 * Represents the completion of an asynchronous operation
 */
interface Promise<T> {
    /**
     * 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>;

    /**
     * Attaches a callback for only the rejection of the Promise.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of the callback.
     */
    catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

PromiseLikeはPromiseのcatchメソッド削った版で、他はまったく一緒だな

まぁ確かにthenの第二引数でもcatchを再現できるので問題では無いのはわかる

thenの第二引数
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

"catchはthenの糖衣構文"
https://jsprimer.net/basic/async/#:~:text=メソッドの糖衣構文であるため同じ動作となります。

だがなぜcatch削ったんだ?PromiseLikeなんてやつを作った理由が気になる

Yug (やぐ)Yug (やぐ)

Gemini 2.0 Pro Experimental 02-05 に聞いた

  • thenだけしか求められない、つまり制約が緩いからいろいろ楽(柔軟)
  • 将来の拡張性が高い

まぁ要は「Thenableな最小限の共通インタフェース / 抽象化レイヤー」として機能させられるという感じっぽい

PromiseLikePromise とは別に存在する主な理由は、相互運用性柔軟性のためです。

  1. 相互運用性 (Interoperability):

    • 異なる Promise 実装との互換性: JavaScript/TypeScript の世界には、標準の Promise 以外にも、Bluebird や Q のようなサードパーティの Promise ライブラリが存在します。これらのライブラリは、標準の Promise とは異なる API を持つことがあります。PromiseLike は、then メソッドさえ持っていれば、それらを Promise と同様に扱えるようにするための「最小限の共通インターフェース」を提供します。
    • Thenable オブジェクト: PromiseLike は、then メソッドを持つオブジェクト(Thenable と呼ばれます)全般を指します。これにより、Promise オブジェクトだけでなく、then メソッドを持つ任意のオブジェクトを、あたかも Promise のように扱うことができます。

    具体的には、Promise.resolve()async/awaitPromiseLike オブジェクトを受け取ることができます。これにより、Promise を返す関数だけでなく、then メソッドを持つオブジェクトを返す関数も、非同期処理のチェーンの中で自然に利用できるようになります。

  2. 柔軟性 (Flexibility):

    • 型システムの制約の緩和: PromiseLikePromise よりも制約が緩いため、より柔軟な型定義が可能になります。Promise インターフェースは catch メソッドも要求しますが、PromiseLikethen メソッドのみを要求します。これにより、then メソッドだけを持つオブジェクトも非同期処理の一部として扱えるようになり、型システムの柔軟性が向上します。
    • 将来の拡張性: Promise の仕様が将来変更された場合でも、PromiseLike はより安定したインターフェースを維持できる可能性があります。PromiseLike は最小限の要件のみを定義しているため、Promise の変更の影響を受けにくいです。

まとめると
PromiseLike は、標準の Promise だけでなく、thenメソッドを持つさまざまなオブジェクト(Thenable)を統一的に扱うための抽象化レイヤーです。これにより、異なるPromise実装間の相互運用性、型システムの柔軟性、そして将来の拡張性を高めています。
Promise を直接使うのではなく、PromiseLike を経由することで、より汎用的で堅牢な非同期処理のコードを書くことができるようになります。

Yug (やぐ)Yug (やぐ)

なぜcatch内の戻り値の型がneverなのに以下のようなtsは実行可能なのかと疑問に思っていたが、geminiに聞いてわかった

neverなのはあくまでデフォルト引数であって、今回のようなケースだと普通にneverにstringが上書きされるという話になる。確かにそうだわ

Promise.reject(1)
  .catch(() => {
    return "1";
  })
  // string | number型になる
  .then((value: string | number) => {
    console.log(value);  // "1"
    console.log(typeof value);  // string
  });

TResult2 = neverってなってるだけでそれはもし何もreturnされなかったらnever型となるという意味なだけであって、returnされたならその型が当然優先されてTResult2になる

はい、おっしゃる通り、 (reason: any) => TResult | PromiseLike<TResult> の部分が、今回の例では () => { return "1"; } と対応しています。そして、TResult のデフォルト型が never であるにも関わらず、return "1" (つまり string 型の値を返す) ことができている点に違和感を持たれるのは、非常に鋭いご指摘です。

この違和感を解消するためには、以下の点を理解することが重要です。

  1. TResult = never はデフォルト型である:

    • catch<TResult = never>(...)TResult = never は、あくまで デフォルト の型引数です。
    • これは、catch メソッドの利用者が TResult の型を明示的に指定 しない 場合に、never が使われるという意味です。
    • しかし、TypeScript は catch に渡されたコールバック関数の 戻り値の型 から TResult推論 します。
  2. 型推論による TResult の上書き:

    • () => { return "1"; } というコールバック関数が catch に渡された場合、TypeScript はこの関数の戻り値の型を string と推論します。
    • そして、推論された string 型が、TResult のデフォルト型である never上書き します。
    • 結果として、catch メソッド全体の型は catch<TResult = string>(...) となり、Promise<T | TResult>Promise<number | string> となります。
  3. | (ユニオン型) の意味:

    • (reason: any) => TResult | PromiseLike<TResult>| はユニオン型を表し、「TResult 型 または PromiseLike<TResult> 型のいずれかの値を返すことができる」という意味です。
    • PromiseLike<TResult>は、TResult 型の値で解決されるPromiseに似たオブジェクト(then メソッドを持つオブジェクト)を返すことができるという意味になります。
    • 今回の例では、return "1"string 型 (そして TResultstring と推論される) なので、このユニオン型の条件を満たしています。

まとめると:

  • TResult = never は、あくまでデフォルトの型であり、型推論によって上書きされる可能性がある。
  • catch に渡されたコールバック関数の戻り値の型が string と推論されたため、TResultstring になり、never ではなくなる。
  • (reason: any) => TResult | PromiseLike<TResult> は、「TResult 型 (この場合は string) または PromiseLike<TResult> 型の値を返すことができる」という意味であり、return "1" はこの条件を満たしている。

このように、TypeScript の型システムは、デフォルト型引数、型推論、ユニオン型などを組み合わせて、柔軟かつ厳密な型チェックを実現しています。never がデフォルトであるにも関わらず、string 型を返せるのは、これらの仕組みが連携して動作しているためです。

Yug (やぐ)Yug (やぐ)

試して得た知見メモる

Promise.reject(1)
  .catch(async () => {
    console.log("1は", 1);
    return new Promise<string>((resolve, reject) => {  // Promiseのネスト
      resolve('fire');
      return 10;  // 意味無い
    })
  })
  .then((value: string) => {  // catch の戻り値の履行時の型は、ネストしたPromiseの解決値に依存
    console.log("2は", value);
  });
  /*
  1は 1
  2は fire
  */
  • promise内でpromiseをネストできるが、その場合はその2番目のpromiseを囲むthen/catchの引数である関数がasyncになる
    • async関数はそもそも「暗黙的にpromiseを返す」という機能
    • 今回は明示的にpromiseを返しているがpromiseを返す時点でasync使うの必須になる
  • catch の戻り値の履行時の型は、ネストしたPromiseの解決値に依存する
    • catch内で直接returnしたものが今回はPromsie<string>なのでそれが採用されて次のthenに渡される
    • 渡ってきたpromiseが履行後stringになるのは当然(resolve使ってるので)
    • なので.then((value: string)にしないといけないということ
Yug (やぐ)Yug (やぐ)

んでそのreturn 10みたいな、returnを拾うのってどうやるんだっけっていう疑問に対しては

new Promise()内のreturnは意味無いのでthen/catch内でreturnしろ
ということになる

  • Promise コンストラクタのコールバック関数(resolve, reject を引数に取る関数)内の return は、Promise の解決値や拒否理由には影響しません
  • resolve または reject を呼び出すことで、Promise の状態と値を決定します。return は単にそのコールバック関数を抜けるだけで、Promise 自体には何も作用しません。

なのでこうやればok

Promise.reject(1)
  .catch(async () => {
    const result = await new Promise<string>((resolve, reject) => {
      resolve('fire');
    })
    console.log('1は', result);  // 1は fire
    return 10;  // catch内でreturnする
  })
  .then((value: number) => {
    console.log("2は", value);  // 2は 10
  });
Yug (やぐ)Yug (やぐ)

これ面白い
https://typescriptbook.jp/reference/asynchronous/promise#待ち受けた非同期処理の拒否の結果をコールバックで実行する---promiseprototypecatch

thenの引数をstring | number型にしないといけないというもの

Promise.resolve(1)
  .catch(() => {
    return "1";
  })
  // string | number型になる
  .then((value: string | number) => {
    console.log(value);
  });

resolveでnumber渡されてるからnumberがあるのは当然として、catch内のreturn値も含んどかないといけないのか

resolve()という処理は解決確定では?と思うところはあるが、resolveはあくまでpromiseを返すのでエラーになる可能性が少しはあるのかもしれない。
なのでエラーになったらcatchに入るので、そのcatchの戻り値であるstringが渡ってくる可能性もあるよねということだろう。なるほど

Yug (やぐ)Yug (やぐ)

ちなみにresolveではなくrejectにすると、問答無用でrejectが確定する(=resolveされる可能性は断たれる)のでcatchの戻り値の型のみ拾うだけで良い

Promise.reject(1)
  .catch(() => {
    return "1";
  })
  // string型で良い
  .then((value: string) => {
    console.log(value);
  });

resolveさんは解決される可能性だけではなく拒否される可能性も考えないといけないのに、catchさんは拒否される可能性だけ考えておけば良くて解決される可能性は考えないで良いと。
世知辛い

Yug (やぐ)Yug (やぐ)

resolveなのに拒否される場合と言うのはこういうやつ
rejectされるpromiseをresolveに渡す

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(new Promise((resolve2, reject2) => {
      reject2('Error in nested promise'); // ネストされた Promise を reject
    }));
  }, 1000);
});

promise1
  .then(value => console.log('Success:', value))
  .catch(reason => console.error('Error:', reason)); // "Error: Error in nested promise" が出力される
Yug (やぐ)Yug (やぐ)

「じゃあそれと同じことrejectでもやればいけるのでは?つまりresolveされるpromiseをrejectに渡せば同じことできないの?」

という疑問に対しては、やはり「それはできない。rejectは即時拒否するので」という話のようだ

残念ながら、resolve に reject される Promise を渡すのと逆のパターン、つまり「reject に resolve される Promise を渡して、最終的に Promise を resolve 扱いにする」という直接的な方法はありません。

reject() 関数は、Promise を 即座に かつ 確定的に rejected 状態にするために設計されています。reject() に何を渡そうとも(resolve される Promise であろうと、他の値であろうと)、その Promise は rejected 状態になり、後からその状態が fulfilled に変わることはありません。

理由:

Promise の状態遷移は一方通行です。

  1. Pending (待機): Promise が作成された直後の初期状態。
  2. Fulfilled (解決済み): resolve() が呼ばれるとこの状態になる。
  3. Rejected (拒否済み): reject() が呼ばれるとこの状態になる。

一度 Fulfilled または Rejected になった Promise は、その状態が 変化することはありませんreject() は、この状態遷移において、強制的に Rejected 状態にするための関数であり、その動作を resolve() のように別の Promise の状態に依存させることはできません。

やっぱresolveと違ってrejectは残酷やな

Yug (やぐ)Yug (やぐ)

then内のreturnとcatch内のreturnは同じようなもんだな
どっちも「次のthenに渡される」ので同じ

then内のreturnはイメージつくけど、catch内のreturnが意味することは何かというと、
「エラーを処理したので正常な状態に復帰できたよ、値返すよ」みたいなニュアンスっぽい

.catch 内で return した場合

  • 次の .then に渡される: .catch のコールバック関数内で return された値は、新しい Promise でラップされ、次の .then のコールバック関数に引数として渡されます。
    * これは、.catch がエラーを処理し、正常な状態に復帰したとみなされるためです。
  • 後続の .catch には渡されない (通常): .catch 内で return された場合、エラーは処理されたとみなされ、後続の .catch はスキップされます。
    * ただし、.catch 内で再度 throw されたり、拒否された Promise が返された場合は、後続の .catch に処理が移ります。
Yug (やぐ)Yug (やぐ)

あー結構掴めてきた

thenもcatchも、内部でpromiseではなく"10"みたいな値をそのままreturnできるが、
then/catch自体のreturn(戻り値)は結局それをpromiseでラップすることになってるという話で、
それが内部実装にそのまま書いてあるだけだ

Yug (やぐ)Yug (やぐ)

勉強になる集

  1. async使っちゃうとthrowした時にNodeがプロセスごと落ちる
    • つまりtryは、throw単体なら対応してcatchに流せるんだけど、asyncの中でthrowされるものに関しては対応できずにNodeごと落ちてcatchにすら流せない
      • なぜそうなってるのか?は内部実装見ないとよくわからんかも。イメージできない...

https://x.com/acomagu/status/1864848760683352073

  1. asyncは厳密にはpromiseを返す訳ではない

https://x.com/nakasyou0/status/1864575156267762110

Yug (やぐ)Yug (やぐ)

解決であって履行ではない...?違いがわからん

Promise.resolve() はプロミスを 解決 します。これはプロミスを履行したり拒否したりすることとは異なります。用語の定義については、プロミスの説明を参照してください。簡単に言うと、 Promise.resolve() は、最終的な状態が他のプロミス、 Thenable オブジェクト、または他の値に依存しているプロミスを返します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve

resolveとthenの違い、rejectとcatcnの違いとか固めときたい

これ読むのが良さそう
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

Yug (やぐ)Yug (やぐ)

この日本語訳は覚えておきたい

Promise の状態は以下のいずれかとなります。

  • 待機 (pending): 初期状態。成功も失敗もしていません。
  • 履行 (fulfilled): 処理が成功して完了したことを意味します。
  • 拒否 (rejected): 処理が失敗したことを意味します。

待機, 履行, 拒否か。じゃあ"履行"というのは「"解決"した結果の状態」と言える?

であれば"履行"は「最終状態」で"解決"はその手段としての「動詞」か?

Yug (やぐ)Yug (やぐ)

へー、"履行"と"拒否"はどっちも"決定"と呼べる

プロミスが履行または拒否のいずれかで、待機以外の状態になった場合は、決定 (settled) と呼ばれます。

良い図解

Yug (やぐ)Yug (やぐ)

なるほど~やっと腑に落ちた、"履行"と"解決"の違い

また、プロミスでは解決 (resolved) という用語が使用されることもあります。これは、プロミスが他のプロミスの最終的な状態と一致するように決定または「ロックイン」され、さらに解決または拒否しても何の影響もないことを意味します。元のプロミスの提案の States and fates の記事には、プロミスの用語についてのより詳細な説明があります。口語的には、「解決された」プロミスは「履行された」プロミスとよく同等に扱われますが、 "States and fates" で示しているように、解決されたプロミスは待機状態になったり拒否されたりすることもあります。次の例を参照してください。

new Promise((resolveOuter) => {
  resolveOuter(
    new Promise((resolveInner) => {
      setTimeout(resolveInner, 1000);
    }),
  );
});

このプロミスは作成された時点ですでに 解決 されていますが(resolveOuter が同期的に呼び出されるため)、別のプロミスで解決されているため、 1 秒後に内側のプロミスが履行されるまで 履行 されません。実際には、「解決」は舞台裏で行われ、観察することができず、履行か拒否かだけを観察することができます。

さっき見たresolve内のpromiseがrejectされればresolveって実質rejectされるよねみたいなややこしい話と関連がある
(まぁrejectされずともpromise渡され得る時点でresolveはfulfilledを名乗ることはやはりできない)

つまりresolve(解決)はそういう余地がある動詞的なものなのだが、fulfilled(履行)は余地がなくresolveが完璧に果たされた後の状態的なものを指すということ

resolve(解決)

余地がある動詞

fulfilled(履行)

余地がない状態

Yug (やぐ)Yug (やぐ)

awaitは内部的にthenが使われてるとは言え、awaitとthenの歴とした違いがあるな

まぁほぼ同じ処理は書けるには書けるんだけども、

// awaitの場合
const data = await Promise.resolve('fire')
console.log('1こめ', data)  // 1こめ fire

// thenで実装しようとした場合
Promise.resolve('fire')
  .then(res => {
    const data = res
    console.log('2こめ', data)  // 2こめ fire
  })

awaitはその行で止まるつまりブロッキングするけどthenはブロッキングしないという違いがあるわ

つまりこういうこと

// awaitだとブロッキングされるので同期処理より非同期処理を優先できる
const data = await Promise.resolve('fire');
console.log("1こめ", data);  // 1こめ fire

// thenだとブロッキングされないので同期処理が優先されてしまう
let data2
Promise.resolve('fire')
  .then(res => {
    data2 = res
  })

console.log("2こめ", data2);  // 2こめ undefined
このスクラップは4ヶ月前にクローズされました