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

`Promise<T>`をラップして、Promise風に振る舞うクラスの定義のしかたを紹介します。Promise風のクラスは、Promise同様に`await`ができるようになります。

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

JavaScriptでは、`then`メソッドを持っているオブジェクトは`Promise`と判断されます。

```javascript
const p = {
  then(resolve) {
    resolve("hello world");
  },
};
console.log(await p);
//=> "hello world"
```

なので、Promiseのように振る舞うクラスを定義する場合は、`then`メソッドを実装すればいいわけです。TypeScriptでは`PromiseLike<T>`インターフェイスがビルトインで提供されているので、これを実装したクラスを定義します。

```typescript
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として扱えるクラスになります。

```typescript
new HelloWorldPromise()
  .then(value => value.toUpperCase())
  .then(value => console.log(value));
//=> "HELLO WORLD"
```

もちろん`await`にも対応できます。

```typescript
const p = new HelloWorldPromise();
console.log(await p);
//=> "Hello World"
```

`Promise<T>`には`catch`や`finally`も生えているので、完全にPromiseを模倣するにはこれらも実装する必要があります。

```typescript
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行程度です。

```javascript
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`そのものです。

```typescript
new HelloWorldPromise({ fails: true })
  .catch((reason) => console.log(reason))
  .finally(() => console.log("finally"));
//=> "ERROR!"
//=> "finally"
```

try-catch構文にも対応できます。

```typescript
try {
  const value: string = await new HelloWorldPromise({ fails: true });
  console.log(value);
} catch (e) {
  console.log(e);
} finally {
  console.log("finally");
}
```

この投稿で紹介したサンプルコードはTypeScript Playgroundで動かせます。

https://www.typescriptlang.org/play?target=9&moduleResolution=99&module=7#code/MYGwhgzhAEASCmIQHsDqyBOIAmAFDyAtgJYTzTGEAOI8h8AdgC4z5GnwAyxA1vADwQmGYgwDmAPmgBvAFDRoGeGGzIGIAJ7QAxFQIkyALmhsDAoSPESA3LPnRgaiwFdgTTAApp0AGZhiIDAAvsbefgEQxgBGyMi0YAzQQQCUMvYKxD7QHuGBqXIKhdBMABakAHS6+hzQALwm1WTlSgBW8G4eAEQAogBKvQDyvQCEncm2RUHQiGRpRQqlFVXss-WmHM3wEHEAbvBdJYgo0ADumDhjE4VB9jf2pYz8vVvOIEwAjHXQFqJiADTQZ4QV5MABMXwY8D2GAkHnS0DUADFXj4ArRsAB+QzwhQAH2yHh2YBAzngxh+4lStSkQJBn3x6zI3D4Txeb3eEmSOOg+OcDGw8FRkOw3PxDFeID+8LUzzabngmOx8x5BKUkDUxj5PAYyBODCpNLZYJVjK4vAEtLeoM5ougfIFQoVtvFSHsyWMptZwPZKstYKkBUKSiYzgwiUWEEqehW8HKDwYHiRKLRCoBMvgcqYCvGtzsCmAYCYwBKXpBEKh8BhcKK6czCqxto8HjV2wYmoY2t1+rqhu9TBNjTNLL9NuVvP5gtETrH0BdIDdHsHgmEv19RoD8ODofDZUjyzM5QLRZKiYYsvaWewOYUdwUQuJmlPiNED40WIJBugO2QxCvKvtk7Ciqc7ug0MbLpYkhzEG8AhmGxS7lGg7lPeSAaE+L5odeSSyHcHiQBoDDAL4fJuMQajZPk9wYFogb5k4-ZEiSZLfCu4hfGAJz+P2kInHARxoOceCDl4vj+IExjCKSSTYfRDDbLQ5QoGIhLEqS2FTEexbZPAVFFI48lxLGykeLpVxTKhmjQXJCnGcgKmdJZGiXLmKQeDmQA