1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「変更の取り消し可能な処理」を実装するためのユーティリティをTypeScriptで書いてみた

Last updated at Posted at 2025-05-19

意識したのは次のこと。

  • このユーティリティを使う側が、ロールバックや変更の取り消し処理を手続き的に書かずに済むようにしたい
  • 心的イメージとして「コマンド」と「シーケンス」を採用。コマンドは一つの処理とその取り消し処理。シーケンスはそのコマンドの連なり

というわけで書いてみた

結論

こうなった

/**
 * 実行したい処理をrunに、戻し処理をrevertに指定する。
 */
export class RevertibleCommand<T> {
  constructor(
    public readonly run: () => Promise<T> | T,
    public readonly revert: () => Promise<void> | void,
  ) { }
}

/**
 * 例外発生時には戻し処理(revert)がまとめて実行される。
 * 正常時は、各コマンドの戻り値をタプルとして返す。
 * 返却値の型はコマンドごとに異なっていても問題ない(タプルが個別の型を持つ)。
 */
export class Sequence<Commands extends RevertibleCommand<any>[]> {
  constructor(
    private readonly commands: readonly [...Commands]
  ) { }

  async run(): Promise<{
    [K in keyof Commands]: Commands[K] extends RevertibleCommand<infer R> ? R : never
  }> {
    const reverts: Array<() => Promise<void> | void> = [];
    const results: unknown[] = [];

    for (const command of this.commands) {
      try {
        const result = await command.run();
        results.push(result);
        reverts.push(command.revert);
      } catch (error) {
        await Promise.allSettled(reverts.map(fn => fn()));
        throw error;
      }
    }

    return results as {
      [K in keyof Commands]: Commands[K] extends RevertibleCommand<infer R> ? R : never
    };
  }
}

使い方

下記のようにして使う。例外発生時には戻し処理(revert)がまとめて実行される。
正常時は、各コマンドの戻り値をタプルとして返す。
返却値の型はコマンドごとに異なっていても問題ない(タプルが個別の型を持つ)。

const command1 = new RevertibleCommand<number>(
  () => { console.log('Run A'); return 10; },
  () => { console.log('Revert A'); },
);

const command2 = new RevertibleCommand<string>(
  () => { console.log('Run B'); return 'OK'; },
  () => { console.log('Revert B'); },
);

const command3 = new RevertibleCommand<boolean>(
  () => { console.log('Run C'); return true; },
  () => { console.log('Revert C'); },
);

const seq = new Sequence([command1, command2, command3]);

(async () => {
  const [a, b, c] = await seq.run();
  console.log(a, b, c);
})();
1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?