🌳

if 文の森から脱出する:`is-kit` で論理的に型ガードを組み立てる

に公開

みなさん、こんにちは!

寒さ対策で毎日四股を踏んでるフロントエンドエンジニアの @nyaomaru です! 🤼

TypeScript での実装で、型ガードが入り組んで if 文が続いてしまうことってありません?

そこで今日は 型ガードを組み立てて if の森から抜け出す 方法について、is-kit の OSS ライブラリを使って紹介していきます。

https://github.com/nyaomaru/is-kit

ほんなら、一緒に見てこな!

🧠 and, or, not の哲学

is-kit の and, or, not は 型ガードそのものを合成できる。

  • and: 全ての条件を満たす(intersection)
  • or: どれかを満たす(union)
  • not: 条件を否定する(negation)

実行時チェックと型の絞り込みの両方を保ったままロジックを構成できる。
つまり、型ガードを“ロジックの文章”として再利用できる DSL(ドメイン特化言語) なんや。

🌲 if 文の森

型ガードによる型の絞り込みが複数になると、if の nest が深くなりがち。

type User = {
  id: string;
  age: number;
  role: 'admin' | 'guest' | 'trial';
};

function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) return false;

  const record = value as Record<string, unknown>;
  return (
    typeof record.id === 'string' &&
    typeof record.age === 'number' &&
    (record.role === 'admin' ||
      record.role === 'guest' ||
      record.role === 'trial')
  );
}

if (isUser(x)) {
  if (x.age >= 20 && x.role === 'admin') {
    // Admin かつ 20 歳以上の場合の処理
  } else if (x.role === 'guest' || x.role === 'trial') {
    // ゲストまたはトライアルの場合の処理
  }
}

こんなネスト地獄、どっかで見たことあるやろ?🌲🌲🌲

struct でベースガードを定義する

まずは is-kit の struct を使って、User のガードを宣言的に書き直してみよか~。
この 1 個をベースにしておけば、後続のロジック合成がめっちゃ楽になるで~!

import { struct, isString, isNumber, oneOfValues } from 'is-kit';

type User = {
  id: string;
  age: number;
  role: 'admin' | 'guest' | 'trial';
};

// struct は readonly プロパティとして推論する。つまり Readonly<User> となる
// これは “構造を守る” 型安全のため。実務でも破壊的変更を防げるんや。
const isUser = struct({
  id: isString,
  age: isNumber,
  role: oneOfValues('admin', 'guest', 'trial'),
});

and を使った複合ガード

and は「最初にベースガード」「続いて refinement 関数」という順番で渡すのがポイントや。

import { and, narrowKeyTo, predicateToRefine } from 'is-kit';

// リードオンリー + role を 'admin' にリテラルで固定した型
type AdminUser = Readonly<User> & { role: 'admin' };

// role='admin' をリテラルに絞る再利用可能なガード
const byRole = narrowKeyTo(isUser, 'role');
const isAdminUser = byRole('admin'); // AdminUser

// さらに年齢 >= 18 を追加
const isAdultAdmin = and(
  isAdminUser,
  predicateToRefine((u: AdminUser) => u.age >= 18)
);

declare const candidate: unknown;

if (isAdultAdmin(candidate)) {
  // candidate は AdminUser
  console.log(candidate.role); // 'admin'
}

and は「安全に合成できる関数」やから、最初のガードが失敗したら次に進まない。
この“短絡評価(short-circuit)”もちゃんと保証されてるねん。

or を使った複合ガード

or は可変長引数。複数のガードを渡すと、その Union 型が推論されるで。

import { or, narrowKeyTo } from 'is-kit';

type GuestUser = Readonly<User> & { role: 'guest' };
type TrialUser = Readonly<User> & { role: 'trial' };

const byRole = narrowKeyTo(isUser, 'role');
const isGuest = byRole('guest'); // GuestUser
const isTrial = byRole('trial'); // TrialUser

const isGuestOrTrial = or(isGuest, isTrial);

declare const value: unknown;

if (isGuestOrTrial(value)) {
  // value は Readonly<User> & { role: 'guest' | 'trial' }
  console.log(value.role);
}

narrowKeyTo はプロパティを安全に比較するためのコンビネータ。第 1 引数に「ベースガード」、第 2 引数に「キー名」を渡して使うで。
or は 「成功した型ガードの型」だけを返す。
つまり上記の例では value.role: 'guest' | 'trial' に自動で絞られる。
この型推論がめっちゃええなぁ~!

not を使った複合ガード

not は与えられたガード/リファインメントを否定するで。

import { and, not } from 'is-kit';

// isGuestOrTrial などの定義を再利用
const isGuestOnly = and(isGuestOrTrial, not(isTrial));

declare const x: unknown;
if (isGuestOnly(x)) {
  x.role; // 'guest'
}

この not は TypeScript の Exclude 型と同じ思想で、「〜ではない」を自然言語みたいに書けるで~!

現場でありがちな複合ロジックを DSL で書く

import { and, narrowKeyTo, or, predicateToRefine } from 'is-kit';

// 役割リテラルのビルダー
const byRole = narrowKeyTo(isUser, 'role');

// admin かつ 18 歳以上
const isAdultAdmin = and(
  byRole('admin'),
  predicateToRefine((u: AdminUser) => u.age >= 18)
);

// guest または trial
const isGuestOrTrial = or(byRole('guest'), byRole('trial'));

こんな風に書けるから、「読みやすい if」→「宣言的ロジック」 に昇格する。
現場での再利用性も爆上がりや!

safeParse で「実行時の型保証」もできる

ガードを関数から返したい時は safeParse を噛ませると綺麗に書けるで~!

import { safeParse } from 'is-kit';

declare const input: unknown;

const adminCheck = safeParse(isAdultAdmin, input);
if (adminCheck.valid) {
  const admin = adminCheck.value;
  // admin は Readonly<User> & { role: 'admin' }
  return handleAdmin(admin);
}

const guestCheck = safeParse(isGuestOrTrial, input);
if (guestCheck.valid) {
  const member = guestCheck.value;
  // member は Readonly<User> & { role: 'guest' | 'trial' }
  return handleGuestLike(member);
}

DSL 的にフローを書けるから、「if 文の森」を抜け出した後の分岐処理もスッキリやな!

まとめ

  • and / or / not は型安全な論理演算子で、短絡評価もそのまま
  • struct や oneOfValues でベースガードを宣言すると再利用性が爆上がり
  • narrowKeyTo を組み合わせればロジックを文章みたいに書ける
  • safeParse を使えば、判定結果を値として扱える

「if (a && b)」を卒業して、「and(a, b)」で語れるコードへ。

シュッて型ガード作りたかったら、ぜひ is-kit 試してみてなぁ~!

if 文の森を抜けたい人は、and, or, not を一度だけ使ってみて。

その瞬間、「これや…!」ってなるで。

https://github.com/nyaomaru/is-kit


🌟 気に入ったらスターで応援してなぁ~!😻

Discussion