44
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScript: Promise風に振る舞うクラスの作り方

Last updated at Posted at 2022-03-29

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>にはcatchfinallyも生えているので、完全に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);
  }
}

catchfinallyまで実装すると、型部分の記述が大きくなりますが、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);
  }
}

catchfinallyまで実装すれば、見た目は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で動かせます。

44
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?