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.
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 from small guards instead of large schema objects
is-kit is probably not the best first choice if you mainly want:
- rich, structured validation errors
- schema-first workflows
- data transformation pipelines
In those cases, a schema validator such as Zod may be a better fit. (Of course, you can combine them π²)
is-kit is meant to take the boring part out of writing guards, while still feeling like normal TypeScript.
Grab a coffee β and let
is-kithandle the repetitive part.
pnpm add is-kit
# or
bun add is-kit
# or
npm install is-kit
# or
yarn add is-kitESM and CJS builds are available for npm consumers, and bundled types are included.
import { and, define, or } from 'jsr:@nyaomaru/is-kit';Start with a plain object guard and parse an unknown value.
import { isNumber, isString, optionalKey, safeParse, struct } from 'is-kit';
declare const input: unknown;
const isUser = struct({
id: isNumber,
name: isString,
nickname: optionalKey(isString)
});
const result = safeParse(isUser, input);
if (result.valid) {
result.value.id;
result.value.name;
result.value.nickname?.toUpperCase();
}This is the core idea of is-kit:
- Build small guards.
- Compose them.
- Reuse them anywhere TypeScript narrowing matters.
If you are new to the library, these are the pieces to remember:
define<T>(fn)turns a boolean check into a typed guard.predicateToRefine(fn)upgrades an existing predicate so it can participate in narrowing chains.struct({...})builds an object-shape guard.safeParse(guard, value)gives you a small tagged result object.assert(guard, value)throws if the value does not match.
Use define when you already know the runtime condition you want.
import { define, isString } from 'is-kit';
const isShortString = define<string>(
(value) => isString(value) && value.length <= 3
);Use and plus predicateToRefine when you want a broad guard first and a narrower condition after that.
import { and, isNumber, predicateToRefine } from 'is-kit';
const isPositiveNumber = and(
isNumber,
predicateToRefine<number>((value) => value > 0)
);Use or and oneOf to combine smaller guards into readable predicates.
import { oneOf, or, isBoolean, isNumber, isString } from 'is-kit';
const isStringOrNumber = or(isString, isNumber);
const isScalar = oneOf(isString, isNumber, isBoolean);Use not(...) when you want the complement of an existing guard or refinement.
Use struct for plain-object payloads. Keys are required by default.
import { isNumber, isString, optionalKey, struct } from 'is-kit';
const isProfile = struct(
{
id: isNumber,
name: isString,
bio: optionalKey(isString)
},
{ exact: true }
);optionalKey(guard) means the property may be missing.
If the property must exist but the value may be undefined, use optional(guard) instead.
import { isString, optional, optionalKey, struct } from 'is-kit';
const isConfig = struct({
label: isString,
subtitle: optional(isString),
note: optionalKey(optional(isString))
});Collection combinators keep your element guards reusable.
import {
arrayOf,
isNumber,
isString,
mapOf,
recordOf,
setOf,
tupleOf
} from 'is-kit';
const isStringArray = arrayOf(isString);
const isPoint = tupleOf(isNumber, isNumber);
const isTagSet = setOf(isString);
const isScoreMap = mapOf(isString, isNumber);
const isStringRecord = recordOf(isString, isString);Use oneOfValues for unions of literal primitives.
import { oneOfValues } from 'is-kit';
const isStatus = oneOfValues('draft', 'published', 'archived');Use the nullish helpers to say exactly what is allowed.
import {
isString,
nonNull,
nullable,
nullish,
optional,
required
} from 'is-kit';
const isNullableString = nullable(isString);
const isNullishString = nullish(isString);
const isOptionalString = optional(isString);
const isDefinedString = required(optional(isString));
const isNonNullString = nonNull(nullable(isString));Use safeParse when you want a result object, and assert when invalid data should stop execution.
import { assert, isString, safeParse } from 'is-kit';
declare const input: unknown;
const parsed = safeParse(isString, input);
if (parsed.valid) {
parsed.value.toUpperCase();
}
assert(isString, input, 'Expected a string');
input.toUpperCase();Use key helpers when the important part of a value is one property.
import {
hasKey,
hasKeys,
isNumber,
isString,
narrowKeyTo,
oneOfValues,
struct
} from 'is-kit';
const isUser = struct({
id: isNumber,
name: isString,
role: oneOfValues('admin', 'member', 'guest')
});
const hasRole = hasKey('role');
const hasRoleAndId = hasKeys('role', 'id');
const byRole = narrowKeyTo(isUser, 'role');
const isAdmin = byRole('admin');
const value: unknown = { id: 1, name: 'nyaomaru', role: 'admin' };
if (hasRole(value)) {
value.role;
}
if (hasRoleAndId(value)) {
value.role;
value.id;
}
if (isAdmin(value)) {
value.role;
value.name;
}Here are the kinds of problems is-kit is especially good at solving:
import { isNumber, isString, safeParse, struct } from 'is-kit';
const isPost = struct({
id: isNumber,
title: isString
});
const parsed = safeParse(isPost, payload);
if (parsed.valid) {
renderPost(parsed.value);
}import { isNumber } from 'is-kit';
const values: unknown[] = [1, 'two', 3];
const numbers = values.filter(isNumber);import { isNumber, isString, narrowKeyTo, oneOfValues, struct } from 'is-kit';
const isEvent = struct({
type: oneOfValues('click', 'submit'),
label: isString,
timestamp: isNumber
});
const byType = narrowKeyTo(isEvent, 'type');
const isSubmitEvent = byType('submit');The library is organized around a few small building blocks:
- Primitives:
isString,isNumber,isBoolean,isInteger, ... - Composition:
define,and,andAll,or,not,oneOf - Object shapes:
struct,optionalKey,hasKey,hasKeys,narrowKeyTo - Collections:
arrayOf,tupleOf,setOf,mapOf,recordOf - Literals:
oneOfValues,equals,equalsBy,equalsKey - Nullish handling:
nullable,nonNull,nullish,optional,required - Result helpers:
safeParse,safeParseWith,assert
For the full API list and dedicated pages, use the docs site below.
For detailed API pages and more examples, see:
https://is-kit-docs.vercel.app/
Requires Node 22 and pnpm 10.12.4.
pnpm lintpnpm buildpnpm testpnpm test:types
See DEVELOPER.md for setup details and CONTRIBUTE.md for contribution workflow.
Pick a guard, compose it, and ship with confidence π
