見出し画像

TSKaigi 2024で気になったTypeScript 関数型スタイルを試してみる

2024年5月11日に開催されたTSKaigiに参加して、一番気になった『TypeScript 関数型バックエンド開発のリアル』のセッションからneverthrowを利用したResult型、andThenによる関数の合成をキャッチアップしてみました。

興味を持ったきっかけ

仕事の実装でフルにTypeScriptを使っており、複数関数を使ったロジックでのエラーハンドリングなどで、try〜catch とifの構造が複雑でどうにも気持ち悪いと感じたのがきっかけ。

元々のコード

  describe("Sample", () => {
    it("元々", async () => {
      interface Coffee {
        id: number;
        name: string;
        status: "prepare" | "drip" | "serve";
      }

      function drip(coffee: Coffee): { data: Coffee | null; error: Error | null } {
        if (coffee.status !== "prepare") {
          return { data: null, error: new Error("status is not prepare") };
        }
        return { data: { ...coffee, status: "drip" }, error: null };
      }

      function serve(coffee: Coffee): { data: Coffee | null; error: Error | null } {
        if (coffee.status !== "drip") {
          return { data: null, error: new Error("status is not drip") };
        }
        return { data: { ...coffee, status: "serve" }, error: null };
      }

      const coffee: Coffee = { id: 1, name: "black", status: "prepare" };
      let result = null;
      try {
        const dripCoffee = drip(coffee);
        if (dripCoffee.error) {
          throw dripCoffee.error;
        }
        if (!dripCoffee.data) {
          throw new Error("data is null");
        }
        const serveCoffee = serve(dripCoffee.data);
        if (serveCoffee.error) {
          throw serveCoffee.error;
        }
        result = serveCoffee.data;
      } catch (error: any) { // エラーの型が・・・
        console.error("Error:", error.message);
      }

      expect(result?.status).toBe("serve");
    });

Coffeeに対して、2つの関数でDripしてServeするみたいな処理の流れ。ご覧のようにtry〜catch, ifの構造でエラーハンドリングを書き出すのがつらい感じに。

neverthrowを試してResult型とInterfaceで状態を型化、そして、andThenで関数を合成

describe("Try andThen With Object", () => {
  it("andThenとObejectの検証", async () => {
    interface Coffee {
      id: number;
      name: string;
      status: string; // order, drip, serveでバリデーションしたい
    }

    interface DripCoffee {
      id: number;
      name: string;
      status: "drip";
    }

    interface ServeCoffee {
      id: number;
      name: string;
      status: "serve";
    }

    function drip(coffee: Coffee): Result<DripCoffee, Error> {
      // interfaceでstatusを決めているので、statusチェックのエラー処理は書かなくて良さそう
      return ok({ ...coffee, status: "drip" });
    }

    function serve(coffee: dripCoffee): Result<ServeCoffee, Error> {
      return ok({ ...coffee, status: "serve" });
    }

    const coffee = ok({ id: 1, name: "black", status: "order" });
    const result = coffee.andThen(drip).andThen(serve);

    expect(result.isOk()).toBe(true);
    expect(result.isErr()).toBe(false);
    if (result.isOk()) {
      expect(result.value.status).toBe("serve");
    }
  });

こんな感じに書き直してみました。
const result = coffee.andThen(drip).andThen(serve);
これが書きたかったことで、実際に書いてみて、ちょっとした感動を覚えました。これが、workflowの力・・・
あと、interfaceに状態ごとに型定義もしてみました。
これが、発表してくれたNaoyaさんの言っていた『関数適用による状態遷移として実装する、型で固める』ってことなのかなぁ〜と、書きながら触りの感覚を掴めた気がします。

書いてみて

試して2日目、まだ良く理解していないことも多いですが、コードの中で処理されていたことが、より明確で安全になったことを実感できました。
型で構造を定義したことで、状態遷移の内容が隠蔽されずに明らかに把握しやすくなる。

最後に

ちょうど自分の中で抱えていた疑問・課題に当てはまるセッションと事例を得られ、TSKaigiの開催・運営にあらためて感謝です!


この記事が気に入ったらサポートをしてみませんか?