かつてコールバック地獄とか呼ばれていたJSの非同期処理は Promise の登場で劇的に改善しました。
さらに async/await の構文の登場によってこれらの非同期処理はさらに簡潔に書けるようになりました。
async/await は何をしているのか ?
内部的には async/await は Promise にある then() というメソッドを処理しています。
ここでTypeScriptの Promise で定義されている then() をみてみます。
interface Promise<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
}
Promise には catch(), finally() といった他のメソッドもあるのですが async/await がみているのは then() だけです。
async/await に対応したクラスをつくりたい !!
then() を Promise と同じシグネチャで実装すれば async/await に対応させることができます。
またTypeScriptにはそのための限定的なインターフェイスとして PromiseLike があります、これを実装すればお手製の async/await に対応したクラスを作ることができます。
(TypeScriptの仕組みで、実際に PromiseLike を実装しなくても、同じメソッドシグネチャがあれば実は対応できます)
interface PromiseLike<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
}
Promise を実装すると catch(), finally() など他のメソッドも実装する必要がありますが、これなら簡単です。
実食
class Gotta implements PromiseLike<string> {
public ingredients: Array<string>;
public constructor(...ingredients: Array<string>) {
this.ingredients = ingredients;
}
public get(): string {
return this.ingredients.join(', ');
}
public then<TResult1 = string, TResult2 = never>(
onfulfilled?: ((value: string) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): PromiseLike<TResult1 | TResult2> {
return new Promise<string>((resolve: (ret: string) => void) => {
resolve('PANCAKE!!!');
}).then(onfulfilled, onrejected);
}
}
それっぽいクラスを作りました。これを以下のように呼び出します。
const gotta: Gotta = new Gotta('flour', 'baking powder', 'egg', 'sugar', 'salt', 'milk', 'vegetable oil');
console.log(gotta.get());
// 'flour, baking powder, egg, sugar, salt, milk, vegetable oil'
console.log(await gotta);
// 'PANCAKE!!!'
やり方次第でもうなんでも await できるようになります。
then() 実装のコツ
then() が要求する引数は null, undefined 可で非常に作りづらいので、一度 Promise のインスタンスを作成して、その then() に引数を任せるのが簡単です。
注意
then() を独自実装したクラスのインスタンスは Promise で包んでしまうと中身が飛び出ます。
console.log(await Promise.resolve(gotta));
// 'PANCAKE!!!'
こんなことするわけないやろ。と思われるかもしれませんが async をつけた非同期の関数でこの問題が起こりえます。
const cooking = async (): Promise<Gotta> => {
return new Gotta('flour', 'baking powder', 'egg', 'sugar', 'salt', 'milk', 'vegetable oil');
}
console.log(await cooking());
// 'PANCAKE!!!'
async の関数内で使うときは独自色を出しすぎた then() を持つクラスのインスタンスの取り扱いに気をつけてください。