DEV Community

nyaomaru
nyaomaru

Posted on

Escaping the Forest of if Statements🌲: Building Logical Type Guards with `is-kit`

Hey everyone 👋
I’m @nyaomaru, a frontend engineer who stays warm every day by practicing sumo squats 🥶💪

Have you ever written complex TypeScript code where type guards get tangled up inside a forest of nested if statements?

Today, let’s explore how to escape that forest by composing type guards logically with the open-source library is-kit.

Let’s dive in together!

 

🧠 The Philosophy of and, or, and not

is-kit provides composable logical operators for type guards:

  • and: all conditions must be true (intersection)
  • or: at least one condition must be true (union)
  • not: negates a condition (negation)

These operators keep both runtime checks and TypeScript narrowing intact.
In other words, they turn type guards into a mini-DSL (domain-specific language) for expressing logic in plain English.

 

🌲 The Forest of if Statements

When type guards pile up, your if statements tend to nest endlessly:

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, age 20+
  } else if (x.role === 'guest' || x.role === 'trial') {
    // Guest or trial user
  }
}
Enter fullscreen mode Exit fullscreen mode

We’ve all been there, a jungle of nested conditions 🌳🌳🌳

 

🏗️ Defining a Base Guard with struct

Let’s rewrite the User guard declaratively using struct.
Once you have this base guard, composing logic becomes a breeze.

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

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

// struct infers readonly properties; effectively Readonly<User>
// This preserves structural type safety and helps prevent breaking changes in practice.
const isUser = struct({
  id: isString,
  age: isNumber,
  role: oneOfValues('admin', 'guest', 'trial'),
});
Enter fullscreen mode Exit fullscreen mode

 

⚙️ Combining Guards with and

and takes guards in order. First the base, then refinements.

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

// Readonly plus role fixed to the 'admin' literal
type AdminUser = Readonly<User> & { role: 'admin' };

// Reusable guard that narrows role to the 'admin' literal
const byRole = narrowKeyTo(isUser, 'role');
const isAdminUser = byRole('admin'); // AdminUser

// Add age >= 18 as an additional refinement
const isAdultAdmin = and(
  isAdminUser,
  predicateToRefine((u: AdminUser) => u.age >= 18)
);

declare const candidate: unknown;

if (isAdultAdmin(candidate)) {
  // candidate: AdminUser
  console.log(candidate.role); // 'admin'
}
Enter fullscreen mode Exit fullscreen mode

and guarantees short-circuit evaluation — if the first guard fails, the rest won’t run.
Just like logical &&, but type-safe and composable.

 

🔁 Combining Guards with or

or accepts multiple guards and infers their union type automatically.

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 is Readonly<User> & { role: 'guest' | 'trial' }
  console.log(value.role);
}
Enter fullscreen mode Exit fullscreen mode

equalsBy safely compares a property based on a base guard.
or returns only the successful type — automatically narrowing role to 'guest' | 'trial'.
This type inference feels so satisfying 😻

 

🚫 Negating Guards with not

not lets you invert an existing guard or refinement.

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

// Reuse definitions like isGuestOrTrial
const isGuestOnly = and(isGuestOrTrial, not(isTrial));

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

Conceptually, not mirrors TypeScript’s Exclude type — letting you express “is not” logic naturally.

 

🧩 Real-World Example: A Readable Logic DSL

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

// Builder for role literals
const byRole = narrowKeyTo(isUser, 'role');

// admin and age 18 or older
const isAdultAdmin = and(
  byRole('admin'),
  predicateToRefine((u: AdminUser) => u.age >= 18)
);

// guest or trial
const isGuestOrTrial = or(byRole('guest'), byRole('trial'));
Enter fullscreen mode Exit fullscreen mode

This turns messy conditionals into declarative, reusable logic.
Perfect for real-world readability and reuse.

 

🛡️ Validating Safely with safeParse

When you want to return a guard’s result, safeParse keeps things neat:

import { safeParse } from 'is-kit';

declare const input: unknown;

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

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

safeParse lets you write flow logic like a DSL.
Clean, composable, and far from the if jungle 🌿

 

🧭 Summary

  • and / or / not are type-safe logical operators with short-circuiting
  • struct and oneOfValues boost reusability with declarative base guards
  • equalsBy and andAll turn logic into readable, sentence-like expressions
  • safeParse lets you safely return and reuse validated results

Time to graduate from if (a && b) and start speaking in and(a, b).

If you want to build snappy, composable type guards, try is-kit
today!


🌟 If you enjoyed this, drop a ⭐️ on the repo! It keeps me motivated! 😸

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


Please check tinyLaunch! 🚀

https://tinylaunch.com/launch/6704

Top comments (0)