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の開催・運営にあらためて感謝です!