Promise<T>
をラップして、Promise風に振る舞うクラスの定義のしかたを紹介します。Promise風のクラスは、Promise同様にawait
ができるようになります。
Promise風に振る舞うクラスの作り方
JavaScriptでは、then
メソッドを持っているオブジェクトはPromise
と判断されます。
const p = {
then(resolve) {
resolve("hello world");
},
};
console.log(await p);
//=> "hello world"
なので、Promiseのように振る舞うクラスを定義する場合は、then
メソッドを実装すればいいわけです。TypeScriptではPromiseLike<T>
インターフェイスがビルトインで提供されているので、これを実装したクラスを定義します。
class HelloWorldPromise implements PromiseLike<string> {
readonly #promise: Promise<string>;
constructor() {
this.#promise = Promise.resolve("Hello World");
}
then<Result1 = string, Result2 = never>(
onFulfilled?:
| ((value: string) => Result1 | PromiseLike<Result1>)
| undefined
| null,
onRejected?:
| ((reason: unknown) => Result2 | PromiseLike<Result2>)
| undefined
| null
): Promise<Result1 | Result2> {
return this.#promise.then(onFulfilled, onRejected);
}
}
then
メソッドの定義は型の部分がだいぶ長いですが、もうこれでPromiseとして扱えるクラスになります。
new HelloWorldPromise()
.then(value => value.toUpperCase())
.then(value => console.log(value));
//=> "HELLO WORLD"
もちろんawait
にも対応できます。
const p = new HelloWorldPromise();
console.log(await p);
//=> "Hello World"
Promise<T>
にはcatch
やfinally
も生えているので、完全にPromiseを模倣するにはこれらも実装する必要があります。
class HelloWorldPromise implements PromiseLike<string> {
readonly #promise: Promise<string>;
constructor({ fails }: { fails: boolean }) {
if (fails) {
this.#promise = Promise.reject("ERROR!");
} else {
this.#promise = Promise.resolve("hello world");
}
}
then<Result1 = string, Result2 = never>(
onFulfilled?:
| ((value: string) => Result1 | PromiseLike<Result1>)
| undefined
| null,
onRejected?:
| ((reason: unknown) => Result2 | PromiseLike<Result2>)
| undefined
| null
): Promise<Result1 | Result2> {
return this.#promise.then(onFulfilled, onRejected);
}
catch<Result = never>(
onRejected?:
| ((reason: unknown) => Result | PromiseLike<Result>)
| undefined
| null
): Promise<string | Result> {
return this.#promise.catch(onRejected);
}
finally(onFinally?: (() => void) | undefined | null): Promise<string> {
return this.#promise.finally(onFinally);
}
}
catch
とfinally
まで実装すると、型部分の記述が大きくなりますが、JavaScriptとしてのコードは賞味20行程度です。
class HelloWorldPromise {
#promise;
constructor({ fails }) {
if (fails) {
this.#promise = Promise.reject("ERROR!");
} else {
this.#promise = Promise.resolve("hello world");
}
}
then(onFulfilled, onRejected) {
return this.#promise.then(onFulfilled, onRejected);
}
catch(onRejected) {
return this.#promise.catch(onRejected);
}
finally(onFinally) {
return this.#promise.finally(onFinally);
}
}
catch
とfinally
まで実装すれば、見た目はPromise
そのものです。
new HelloWorldPromise({ fails: true })
.catch((reason) => console.log(reason))
.finally(() => console.log("finally"));
//=> "ERROR!"
//=> "finally"
try-catch構文にも対応できます。
try {
const value: string = await new HelloWorldPromise({ fails: true });
console.log(value);
} catch (e) {
console.log(e);
} finally {
console.log("finally");
}
この投稿で紹介したサンプルコードはTypeScript Playgroundで動かせます。