npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

is-kit

v1.15.1

Published

Zero-dependency TypeScript runtime validation toolkit for composable type guards and natural type narrowing.

Readme

is-kit

is-kit is a lightweight, zero-dependency toolkit for building reusable TypeScript type guards.

is-kit is not just a collection of isX helpers. Its main focus is composing small runtime checks into reusable guards while preserving useful TypeScript narrowing.

It helps you write small isFoo functions, compose them into richer runtime checks, refine properties on values you already know about, and keep TypeScript narrowing natural inside regular control flow. Use it at runtime boundaries when needed, without requiring a schema-first workflow.

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
  • Use refineKey to narrow a child property while preserving its parent type
  • Validate object shapes and collections when that is useful
  • Parse or assert unknown values without a large schema framework

Best for app-internal narrowing, filtering, and reusable guards.

🤔 Why use is-kit?

Many TypeScript projects eventually grow a utils/is.ts, libs/is.ts, or guards.ts file. It fills with hand-written guards that solve the same problems across projects, are tedious to rewrite, and need manual care to keep their TypeScript narrowing correct as they evolve.

is-kit exists to make those guards reusable, composable, and easier to maintain. The goal is not to replace every validation library; it is to make reusable TypeScript type guards easier to build, compose, and maintain.

It is a good fit when you want to:

  • write reusable isX functions 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, including property refinements

If you only need a few standalone isX checks, a smaller type-check utility may be simpler. is-kit becomes useful when those checks need to be composed, refined, and reused across your application while preserving TypeScript narrowing.

Schema validators such as Zod, Valibot, and ArkType often optimize for a different workflow. They may be a better fit when you mainly want:

  • rich, structured validation errors
  • schema-first workflows
  • data transformation pipelines

is-kit is aimed at reusable runtime predicates that behave naturally as TypeScript type guards. The approaches can also be used together: use a schema validator where its parsing model helps, and compose is-kit guards wherever ordinary narrowing and reusable predicates are the better fit.

is-kit is meant to take the repetitive part out of writing guards while still feeling like normal TypeScript.

📥 Install

pnpm add is-kit
# or
bun add is-kit
# or
npm install is-kit
# or
yarn add is-kit
# or
vlt install is-kit

ESM and CJS builds are available for npm consumers, and bundled types are included. The published declarations support TypeScript 5.7 and newer. CI compiles the packed package against every TypeScript minor from 5.7 through the current stable release.

TypeScript 7 can type-check is-kit declarations, but TypeScript 7.0 does not ship the legacy JavaScript Compiler API. Tools that need that API can follow the official TypeScript 7 side-by-side guidance and use @typescript/typescript6 for programmatic access.

vlt requires registry configuration before installing a package by name. Run vlt setup once if a registry has not been configured yet.

JSR

Install the JSR package with vlt:

vlt install jsr:@nyaomaru/is-kit

Or import it directly in runtimes that support jsr: specifiers:

import { and, define, or } from 'jsr:@nyaomaru/is-kit';

✨ Quick Start

Start by composing small guards.

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

const isId = or(isString, isNumber);

const isPositiveNumber = and(
  isNumber,
  predicateToRefine<number>((value) => value > 0)
);

You can also refine one property of an already-typed parent value.

import { isString, refineKey } from 'is-kit';

type Item = {
  value: string | number;
  id: number;
};

const hasStringValue = refineKey('value', isString);

declare const items: Item[];

const textItems = items.filter(hasStringValue);
// Array<Item & { value: string }>

The original Item type is preserved while its checked value property is narrowed. This lets a reusable guard carry child narrowing back to the parent through filter, find, and control flow.

For an unknown value at a runtime boundary, build an object guard and parse it.

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:

  1. Build small guards.
  2. Compose them.
  3. Reuse them anywhere TypeScript narrowing matters.

ESLint integration

eslint-plugin-is-kit detects ambiguous or redundant array predicates and can suggest reusable is-kit guards when they preserve runtime behavior and useful narrowing.

values.filter(Boolean); // May also remove "", 0, false, and NaN.
values.filter(isNotNil); // Removes only null and undefined.
pnpm add -D eslint-plugin-is-kit

See the plugin setup and rule reference.

🧭 Guard Composition Guide

When writing reusable guards with is-kit, start from the library primitives: use define for custom runtime checks, and use logic combinators such as and, or, and not when combining existing guards. This keeps the result reusable as a named guard and preserves the type-level intent in hover, completion, and generated declarations.

import {
  and,
  define,
  isNil,
  isNumber,
  isString,
  nullish,
  or,
  predicateToRefine
} from 'is-kit';

const isId = or(isString, isNumber);
const isNullishString = nullish(isString);

const isSlug = define<string>(
  (value) => isString(value) && /^[a-z0-9-]+$/.test(value)
);

const isPositiveNumber = and(
  isNumber,
  predicateToRefine<number>((value) => value > 0)
);

isNil(null); // true
isNil(undefined); // true
isId('user-1'); // true
isNullishString(undefined); // true
isSlug('release-110'); // true
isPositiveNumber(1); // true

Prefer these forms when generating or reviewing code:

declare const value: unknown;

// Prefer
const isId = or(isString, isNumber);
const isMaybeName = nullish(isString);
const isSlug = define<string>(
  (value) => isString(value) && /^[a-z0-9-]+$/.test(value)
);

// Avoid
const isId = (value: unknown) => isString(value) || isNumber(value);
const isMaybeName = (value: unknown) => value == null || isString(value);
const isSlug = (value: unknown): value is string =>
  isString(value) && /^[a-z0-9-]+$/.test(value);

AI agent setup

Recommended: Codex Marketplace plugin

Install the is-kit plugin from this repository's Codex Marketplace:

codex plugin marketplace add nyaomaru/is-kit
codex plugin add is-kit@is-kit

This is the primary distribution channel for the use-is-kit skill. It gives Codex an on-demand workflow for selecting guards, replacing manual runtime checks, and validating object boundaries with is-kit.

Compatibility: instruction-only setup

For agents that do not support skills or plugins, add the lightweight selection and usage rules to the repository's agent instructions:

npx --yes is-kit@latest init-agent

The command updates an is-kit-managed section in AGENTS.md without overwriting other instructions. If a repository uses CLAUDE.md, target it explicitly:

npx --yes is-kit@latest init-agent --target claude

See docs/agent-rules.md for the installed rules.

⌚ A 30-second Mental Model

If you are new to the library, these are the pieces to remember:

  • define<T>(fn) turns a boolean check into a typed guard.
  • lazy(factory) defers a guard definition so recursive structures can refer to themselves.
  • predicateToRefine(fn) upgrades an existing predicate so it can participate in narrowing chains.
  • refineKey(key, guard) carries a child refinement back onto its parent type.
  • 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.

⚒️ Common Usage

1. Create a custom guard

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
);

2. Add refinements to an existing guard

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)
);

3. Compose multiple guards

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.

4. Validate object shapes

Use struct for plain-object payloads. Keys are required by default.

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

const isProfile = struct(
  {
    id: isNumber,
    name: isString,
    bio: optionalKey(isString)
  },
  { exact: true }
);

const isConfig = struct({
  label: isString,
  subtitle: optional(isString),
  note: optionalKey(optional(isString))
});

optionalKey(guard) means the property may be missing.

Use struct(schema, { exact: true }) when extra own enumerable string keys should be rejected. Exact mode follows Object.keys(...) semantics, so symbol keys and non-enumerable properties are outside its key matching.

If the property must exist but the value may be undefined, use optional(guard) instead.

5. Validate arrays, tuples, maps, sets, and records

Collection combinators keep your element guards reusable.

import {
  arrayOf,
  isNumber,
  isString,
  mapOf,
  nonEmptyArrayOf,
  recordOf,
  setOf,
  tupleOf
} from 'is-kit';

const isStringArray = arrayOf(isString);
const isNonEmptyTagList = nonEmptyArrayOf(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');

6. Validate recursive structures

Use lazy when a guard needs to refer to itself. The factory runs on first use, and the resulting guard is cached.

import { arrayOf, isString, lazy, typedStruct } from 'is-kit';
import type { Predicate } from 'is-kit';

type Tree = {
  readonly value: string;
  readonly children: readonly Tree[];
};

const isTree: Predicate<Tree> = lazy(() =>
  typedStruct<Tree>()({
    value: isString,
    children: arrayOf(isTree)
  })
);

lazy does not detect circular references in the input value.

7. Refine properties on existing types

Use the property helpers when an object is already typed and one child value needs additional narrowing.

import { isString, refineDefinedKey, refineIndex, refineKey } from 'is-kit';

type Item = {
  readonly value: string | number;
  readonly label?: string | number;
};

const hasStringValue = refineKey('value', isString);
const hasDefinedStringLabel = refineDefinedKey('label', isString);
const hasStringAtZero = refineIndex(0, isString);

declare const items: readonly Item[];
const textItems = items.filter(hasStringValue);
// Array<Item & Record<'value', string>>

The important part is reusable parent narrowing: the predicate retains both the original Item type and the checked property type through filter, find, and control-flow branches without a handwritten intersection annotation.

  • refineKey refines one required property using normal property access.
  • refineDefinedKey returns false for a missing or undefined property.
  • refineIndex returns false for an out-of-bounds, sparse, or undefined own element.

The property helpers accept inherited values and accessors. The index helper requires an own element so a sparse hole cannot pass through an inherited numeric property.

Each key or index must identify one concrete runtime location. This keeps one successful lookup from incorrectly narrowing multiple properties.

See Refine properties on existing TypeScript types for required, optional, indexed, nested, and literal examples. The TypeScript Compiler API is one advanced application of this generic pattern.

8. Handle null and undefined explicitly

Use the nullish helpers to say exactly what is allowed.

import {
  isNil,
  isNotNil,
  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));

isNil(null); // true
isNil(undefined); // true
isNil(0); // false

const values: Array<string | null | undefined> = ['value', null, undefined];
const presentValues = values.filter(isNotNil); // string[]

Use isNil for a direct nullish check instead of hand-rolling isNull(x) || isUndefined(x). Use isNotNil for the inverse check or to remove nullish values while preserving Array.prototype.filter narrowing.

9. Parse or assert unknown input

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();

10. Decode and validate JSON input

Use safeJsonParse at a JSON text boundary. It decodes the text, treats the result as unknown, and only returns the value after the guard accepts it. Invalid JSON and guard mismatches both return { valid: false }; values are not coerced to satisfy the guard.

import { isString, safeJsonParse, typedStruct } from 'is-kit';

type User = {
  id: string;
  name: string;
};

const isUser = typedStruct<User>()({
  id: isString,
  name: isString
});

declare const input: string;
const result = safeJsonParse(input, isUser);

if (result.valid) {
  result.value.name.toUpperCase();
}

safeJsonParse is a decode-then-guard helper. It does not perform schema coercion and does not depend on a transport or schema format such as HTTP or OpenAPI.

11. Validate an exhaustive discriminated union

Use discriminatedUnion<T>() with typedStruct to keep both the branch fields and the union coverage aligned with an existing TypeScript type.

import { discriminatedUnion, isNumber, oneOfValues, typedStruct } from 'is-kit';

type Event =
  { kind: 'click'; x: number; y: number } | { kind: 'scroll'; delta: number };

const eventUnion = discriminatedUnion<Event>();

export const isEvent = eventUnion('kind', ['click', 'scroll'], {
  click: typedStruct<Extract<Event, { kind: 'click' }>>()({
    kind: oneOfValues('click'),
    x: isNumber,
    y: isNumber
  }),
  scroll: typedStruct<Extract<Event, { kind: 'scroll' }>>()({
    kind: oneOfValues('scroll'),
    delta: isNumber
  })
});

The value tuple and object keys must each exactly match the union's discriminant values, so adding or removing an Event member produces a type error until both are updated. The tuple makes finite coverage explicit, so broad string, number, and symbol discriminants and infinite template-literal types are rejected. Boolean branches use the true and false object keys, which is useful for Result types with an ok field. Discriminants that would coerce to the same object key, such as 1 and '1', are rejected because they cannot be represented as separate branches. For a __proto__ discriminant, write the branch as ['__proto__']: guard (or use a null-prototype map); the uncomputed object-literal form changes the object's prototype instead of creating a branch entry.

12. Narrow object keys

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;
}

Singleton literal keys preserve key-specific narrowing. Union, broad, patterned, or branded key domains remain usable as runtime checks, but they narrow only to an object—or to the base guard type for narrowKeyTo—because one runtime key cannot prove that every possible key was checked.

🌍 Real-world use cases

Here are the kinds of problems is-kit is especially good at solving:

Typed object guard checks

import {
  isNumber,
  isString,
  optionalKey,
  safeParse,
  typedStruct
} from 'is-kit';

type PostResponse = ApiResponse<'/posts/{id}', 'get'>;

const isPost = typedStruct<PostResponse>()({
  id: isNumber,
  title: isString,
  summary: optionalKey(isString)
});

const payload: unknown = await fetchPost();
const parsed = safeParse(isPost, payload);

if (parsed.valid) {
  renderPost(parsed.value);
}

typedStruct<T>() is a small helper for keeping hand-written struct guards in sync with an existing object type. Optional keys in T must still be declared with optionalKey(...); this makes drift visible when the target type changes. OpenAPI-generated response types are one useful case, but the helper is not an OpenAPI validator or schema generator. Drift detection is limited to string-keyed properties; numeric and symbol properties are excluded from the checked shape and cannot be validated by the resulting guard.

Safe array filtering

import { isNumber } from 'is-kit';

const values: unknown[] = [1, 'two', 3];
const numbers = values.filter(isNumber);

Narrowing by discriminant

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');

🎯 API Overview

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, typedStruct, discriminatedUnion, optionalKey, hasKey, hasKeys, narrowKeyTo, refineKey, refineDefinedKey, refineIndex
  • Collections: arrayOf, nonEmptyArrayOf, tupleOf, setOf, mapOf, recordOf
  • Literals: oneOfValues, equals, equalsBy, equalsKey
  • Nullish handling: isNil, isNotNil, nullable, nonNull, nullish, optional, required
  • Result helpers: safeParse, safeParseWith, safeJsonParse, assert

For the full API list and dedicated pages, use the docs site below.

Public types

Start with the primary types for guard authoring, parsing, and schema inference:

  • Predicate, Guard, Refinement, Refine, ParseResult, Primitive
  • InferSchema, StructOptions, TypedStructShape, TypedStructFields

Advanced building blocks support reusable wrappers and type-level extensions:

  • GuardedOf, GuardedWithin, OutOfGuards, RefineChain, ChainResult
  • OptionalSchemaField, SchemaField, Schema, NoExtraKeys
  • OptionalObjectKeys, RequiredObjectKeys

Both groups are supported public API and follow semantic versioning. The classification only controls discoverability. SchemaShape remains internal and is not exported from the package root.

v2 migration notice

Six low-level utility adapters remain available in v1 for compatibility but are deprecated and will no longer be exported from the package root in v2:

| Deprecated export | Migration path | | ------------------------- | ---------------------------------------------------------------------- | | everyArrayValue | Use Array.prototype.every or the arrayOf guard. | | everyTupleValue | Use the tupleOf guard or compare tuple values directly. | | everySetValue | Use the setOf guard or iterate the set directly. | | everyMapEntry | Use the mapOf guard or iterate the map directly. | | everyOwnEnumerableEntry | Use the recordOf guard or Object.entries(value).every(...). | | toBooleanPredicates | Use the original predicate array directly; this helper is an identity. |

The underlying internal helpers may remain in the implementation. Only their public root exports are planned for removal.

📚 Full Documentation

For detailed API pages and more examples, see:

Start with How to safely filter null and undefined from arrays in TypeScript.

👨‍💻 Development

Requires Node 22.22.0 and pnpm 11.2.2.

  • pnpm lint
  • pnpm build
  • pnpm test
  • pnpm test:types
  • pnpm test:package

See DEVELOPER.md for setup details and CONTRIBUTE.md for contribution workflow.

Pick a guard, compose it, and ship with confidence 🚀

Star History