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
}
}
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'),
});
⚙️ 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'
}
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);
}
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'
}
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'));
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);
}
safeParse lets you write flow logic like a DSL.
Clean, composable, and far from the if jungle 🌿
🧭 Summary
-
and/or/notare type-safe logical operators with short-circuiting -
structandoneOfValuesboost reusability with declarative base guards -
equalsByandandAllturn logic into readable, sentence-like expressions -
safeParselets 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! 🚀
Top comments (0)