Hi everyone!
I’m a frontend engineer who sometimes wonders:
“Am I using AI… or is AI using me?”
When writing runtime validation in TypeScript,
Zod is usually the first choice.
There are other options like Valibot and ArkType,
but today I want to look at something a bit different:
nyaomaru
/
is-kit
Lightweight, zero-dependency toolkit for building `isFoo` style type guards in TypeScript. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨. npm -> https://www.npmjs.com/package/is-kit
is-kit
is-kit is a lightweight, zero-dependency toolkit for building reusable TypeScript type guards.
It helps you write small isFoo functions, compose them into richer runtime checks, and keep TypeScript narrowing natural inside regular control flow.
Runtime-safe 🛡️, composable 🧩, and ergonomic ✨ without asking you to adopt a heavy schema workflow.
- Build and reuse typed guards
-
Compose guards with
and,or,not,oneOf - Validate object shapes and collections
-
Parse or assert
unknownvalues without a large schema framework
Best for app-internal narrowing, filtering, and reusable guards.
🤔 Why use is-kit?
Tired of rewriting the same isFoo checks again and again?
is-kit is a good fit when you want to:
-
write reusable
isXfunctions instead of one-off inline checks - keep runtime validation lightweight and dependency-free
-
narrow values directly in
if,filter, and other TypeScript control flow - compose validation logic…
At first glance, is-kit and Zod look similar.
But their design philosophy is very different.
-
is-kit→ a toolkit to compose type guards -
Zod→ a library to define schemas and validate data
So instead of asking “which is better?”,
let’s compare them from a practical angle:
When is each one easier to write?
We’ll look at:
- Code size
- Readability
- How they handle types
Same Example
Let’s validate a simple User.
🔧 is-kit
import {
and,
isInteger,
isString,
oneOfValues,
optionalKey,
predicateToRefine,
safeParse,
struct,
} from "is-kit";
const isPositiveInt = and(
isInteger,
predicateToRefine<number>((v) => v > 0),
);
const isUser = struct({
id: isPositiveInt,
name: isString,
role: oneOfValues(["admin", "member"] as const),
nickname: optionalKey(isString),
});
const result = safeParse(isUser, input);
if (result.valid) {
result.value.role;
}
💎 Zod
import { z } from "zod";
const UserSchema = z.object({
id: z.number().int().positive(),
name: z.string(),
role: z.enum(["admin", "member"]),
nickname: z.string().optional(),
});
const result = UserSchema.safeParse(input);
if (result.success) {
result.data.role;
}
You can already see the difference:
-
is-kit→ compose small guards (function-based) -
Zod→ define a schema (declarative)
1. Code Size
Zod often looks shorter.
Why?
Because you can stack constraints in one place:
z.string().min(1).max(20).regex(...)
z.number().int().positive()
This works very well for:
- Forms
- API validation
- Input boundaries
On the other hand, is-kit shines when you want to reuse guards.
if (isUser(value)) {
// already narrowed
}
const users = values.filter(isUser);
You can directly pass guards into control flow.
With Zod, you usually go through safeParse,
so it feels more like “calling validation”.
👉 Summary:
- Want centralized validation rules →
Zod - Want reusable guards in logic →
is-kit
2. Readability
This depends on what “readable” means to you.
💎 Zod
All rules are in one place:
const UserSchema = z.object({ ... });
You can instantly see:
👉 “What shape is valid?”
Great for boundaries.
🔧 is-kit
is-kit feels like normal TypeScript control flow.
It also lets you compose guards step by step:
import { hasKey, narrowKeyTo } from "is-kit";
const hasRole = hasKey("role");
if (hasRole(value)) {
value.role;
}
const byRole = narrowKeyTo(isUser, "role");
const isAdmin = byRole("admin");
if (isAdmin(value)) {
value.role;
value.name;
}
You can:
- Narrow the whole shape (
isUser) - Focus on a specific key (
role) - Narrow further (
admin)
This fits naturally with:
“Receive unknown → narrow step by step”
👉 Summary:
- Want a clear spec of valid data →
Zod - Want natural control flow narrowing →
is-kit
3. How Types Are Handled
This is where the biggest difference appears.
💎 Zod → Schema → Type
type User = z.infer<typeof UserSchema>;
- Define schema
- Generate type
Best when you want:
“Validation and types in one place”
🔧 is-kit → Guard → Narrowing
is-kit focuses on control flow:
if (isUser(input)) {
input.role;
}
The type is narrowed automatically.
You can also extract the type:
import type { GuardedOf } from "is-kit";
type User = GuardedOf<typeof isUser>;
But the main strength is not type generation.
👉 It’s this:
TypeScript understands your logic as you narrow values
Core Difference
-
Zod→ strong at schema → type -
is-kit→ strong at guard → control flow narrowing
Summary
| Perspective | Zod 💎 | is-kit 🔧 |
|---|---|---|
| Code size | Great for schema | Great for reuse |
| Readability | Clear validation rules | Natural control flow |
| Type handling | Schema-driven types | Control flow narrowing |
When to Use Each
My approach:
- Use
Zodfor external inputs- API
- forms
- environment variables
- Use
is-kitfor internal logic- condition branches
- filtering
- composing guards
They Are Not Competitors
These tools are not direct competitors.
They work well together.
👉 Clean separation:
-
Zod→ validate at boundaries -
is-kit→ refine inside your app
Conclusion
is-kit is not a lightweight version of Zod.
It focuses on a different idea:
Making type guards feel natural in TypeScript
And Zod is still extremely strong:
Schema-first validation with type safety
The real question is not:
“Which one is better?”
But:
“Where do you enforce type safety in your design?”
Try it if you're interested:



Top comments (0)