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で動かせます。