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

インタフェース以外もいろいろ調べたり試したりする
まずはインタフェース
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の第二引数
"catchはthenの糖衣構文"
だがなぜcatch削ったんだ?PromiseLikeなんてやつを作った理由が気になる

Gemini 2.0 Pro Experimental 02-05 に聞いた
- thenだけしか求められない、つまり制約が緩いからいろいろ楽(柔軟)
- 将来の拡張性が高い
まぁ要は「Thenableな最小限の共通インタフェース / 抽象化レイヤー」として機能させられるという感じっぽい
PromiseLike
がPromise
とは別に存在する主な理由は、相互運用性と柔軟性のためです。
相互運用性 (Interoperability):
- 異なる Promise 実装との互換性: JavaScript/TypeScript の世界には、標準の
Promise
以外にも、Bluebird や Q のようなサードパーティの Promise ライブラリが存在します。これらのライブラリは、標準のPromise
とは異なる API を持つことがあります。PromiseLike
は、then
メソッドさえ持っていれば、それらをPromise
と同様に扱えるようにするための「最小限の共通インターフェース」を提供します。- Thenable オブジェクト:
PromiseLike
は、then
メソッドを持つオブジェクト(Thenable と呼ばれます)全般を指します。これにより、Promise
オブジェクトだけでなく、then
メソッドを持つ任意のオブジェクトを、あたかもPromise
のように扱うことができます。具体的には、
Promise.resolve()
やasync/await
はPromiseLike
オブジェクトを受け取ることができます。これにより、Promise
を返す関数だけでなく、then
メソッドを持つオブジェクトを返す関数も、非同期処理のチェーンの中で自然に利用できるようになります。柔軟性 (Flexibility):
- 型システムの制約の緩和:
PromiseLike
はPromise
よりも制約が緩いため、より柔軟な型定義が可能になります。Promise
インターフェースはcatch
メソッドも要求しますが、PromiseLike
はthen
メソッドのみを要求します。これにより、then
メソッドだけを持つオブジェクトも非同期処理の一部として扱えるようになり、型システムの柔軟性が向上します。- 将来の拡張性:
Promise
の仕様が将来変更された場合でも、PromiseLike
はより安定したインターフェースを維持できる可能性があります。PromiseLike
は最小限の要件のみを定義しているため、Promise
の変更の影響を受けにくいです。まとめると
PromiseLike
は、標準のPromise
だけでなく、then
メソッドを持つさまざまなオブジェクト(Thenable)を統一的に扱うための抽象化レイヤーです。これにより、異なるPromise実装間の相互運用性、型システムの柔軟性、そして将来の拡張性を高めています。
Promise
を直接使うのではなく、PromiseLike
を経由することで、より汎用的で堅牢な非同期処理のコードを書くことができるようになります。

なぜ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
型の値を返す) ことができている点に違和感を持たれるのは、非常に鋭いご指摘です。この違和感を解消するためには、以下の点を理解することが重要です。
TResult = never
はデフォルト型である:
catch<TResult = never>(...)
のTResult = never
は、あくまで デフォルト の型引数です。- これは、
catch
メソッドの利用者がTResult
の型を明示的に指定 しない 場合に、never
が使われるという意味です。- しかし、TypeScript は
catch
に渡されたコールバック関数の 戻り値の型 からTResult
を 推論 します。型推論による
TResult
の上書き:
() => { return "1"; }
というコールバック関数がcatch
に渡された場合、TypeScript はこの関数の戻り値の型をstring
と推論します。- そして、推論された
string
型が、TResult
のデフォルト型であるnever
を 上書き します。- 結果として、
catch
メソッド全体の型はcatch<TResult = string>(...)
となり、Promise<T | TResult>
はPromise<number | string>
となります。
|
(ユニオン型) の意味:
(reason: any) => TResult | PromiseLike<TResult>
の|
はユニオン型を表し、「TResult
型 またはPromiseLike<TResult>
型のいずれかの値を返すことができる」という意味です。PromiseLike<TResult>
は、TResult
型の値で解決されるPromiseに似たオブジェクト(then メソッドを持つオブジェクト)を返すことができるという意味になります。- 今回の例では、
return "1"
はstring
型 (そしてTResult
はstring
と推論される) なので、このユニオン型の条件を満たしています。まとめると:
TResult = never
は、あくまでデフォルトの型であり、型推論によって上書きされる可能性がある。catch
に渡されたコールバック関数の戻り値の型がstring
と推論されたため、TResult
はstring
になり、never
ではなくなる。(reason: any) => TResult | PromiseLike<TResult>
は、「TResult
型 (この場合はstring
) またはPromiseLike<TResult>
型の値を返すことができる」という意味であり、return "1"
はこの条件を満たしている。このように、TypeScript の型システムは、デフォルト型引数、型推論、ユニオン型などを組み合わせて、柔軟かつ厳密な型チェックを実現しています。
never
がデフォルトであるにも関わらず、string
型を返せるのは、これらの仕組みが連携して動作しているためです。

試して得た知見メモる
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)
にしないといけないということ
- catch内で直接returnしたものが今回は

んでその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
});

これ面白い
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が渡ってくる可能性もあるよねということだろう。なるほど

ちなみにresolveではなくrejectにすると、問答無用でrejectが確定する(=resolveされる可能性は断たれる)のでcatchの戻り値の型のみ拾うだけで良い
Promise.reject(1)
.catch(() => {
return "1";
})
// string型で良い
.then((value: string) => {
console.log(value);
});
resolveさんは解決される可能性だけではなく拒否される可能性も考えないといけないのに、catchさんは拒否される可能性だけ考えておけば良くて解決される可能性は考えないで良いと。
世知辛い

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" が出力される

「じゃあそれと同じことrejectでもやればいけるのでは?つまりresolveされるpromiseをrejectに渡せば同じことできないの?」
という疑問に対しては、やはり「それはできない。rejectは即時拒否するので」という話のようだ
残念ながら、
resolve
に reject される Promise を渡すのと逆のパターン、つまり「reject
に resolve される Promise を渡して、最終的に Promise を resolve 扱いにする」という直接的な方法はありません。
reject()
関数は、Promise を 即座に かつ 確定的に rejected 状態にするために設計されています。reject()
に何を渡そうとも(resolve される Promise であろうと、他の値であろうと)、その Promise は rejected 状態になり、後からその状態が fulfilled に変わることはありません。理由:
Promise の状態遷移は一方通行です。
- Pending (待機): Promise が作成された直後の初期状態。
- Fulfilled (解決済み):
resolve()
が呼ばれるとこの状態になる。- Rejected (拒否済み):
reject()
が呼ばれるとこの状態になる。一度 Fulfilled または Rejected になった Promise は、その状態が 変化することはありません。
reject()
は、この状態遷移において、強制的に Rejected 状態にするための関数であり、その動作をresolve()
のように別の Promise の状態に依存させることはできません。
やっぱresolveと違ってrejectは残酷やな

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
に処理が移ります。

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

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

解決であって履行ではない...?違いがわからん
Promise.resolve() はプロミスを 解決 します。これはプロミスを履行したり拒否したりすることとは異なります。用語の定義については、プロミスの説明を参照してください。簡単に言うと、 Promise.resolve() は、最終的な状態が他のプロミス、 Thenable オブジェクト、または他の値に依存しているプロミスを返します。
resolveとthenの違い、rejectとcatcnの違いとか固めときたい
これ読むのが良さそう

この日本語訳は覚えておきたい
Promise
の状態は以下のいずれかとなります。
- 待機 (pending): 初期状態。成功も失敗もしていません。
- 履行 (fulfilled): 処理が成功して完了したことを意味します。
- 拒否 (rejected): 処理が失敗したことを意味します。
待機, 履行, 拒否か。じゃあ"履行"というのは「"解決"した結果の状態」と言える?
であれば"履行"は「最終状態」で"解決"はその手段としての「動詞」か?

へー、"履行"と"拒否"はどっちも"決定"と呼べる
プロミスが履行または拒否のいずれかで、待機以外の状態になった場合は、決定 (settled) と呼ばれます。
良い図解

なるほど~やっと腑に落ちた、"履行"と"解決"の違い
また、プロミスでは解決 (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(履行)
余地がない状態

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