DEV Community

Cover image for is-kit Reached 50 Stars โญ Hereโ€™s How We Use It in Production
nyaomaru
nyaomaru

Posted on

is-kit Reached 50 Stars โญ Hereโ€™s How We Use It in Production

Hoi hoi!

I'm @nyaomaru, a frontend engineer who is trying to lose weight. ๐Ÿ–๐Ÿ™€

I maintain a type guard library, is-kit.

I spend an unreasonable amount of time asking

โ€œBut what if this value is actually unknown???โ€

Recently, is-kit crossed 50 GitHub stars ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰

A star is not a benchmark. And 50 stars do not suddenly make a library production-ready.

But each one still means

โ€œSomeone found this idea useful.โ€

That makes me very happy!! Each star gives me more motivation to keep improving the library!!!!

There is also something more concrete I want to share

is-kit is now used in a production TypeScript application serving more than 100,000 users. ๐Ÿš€

This article explains:

  • What problem we had
  • How we introduced is-kit
  • What actually changed
  • Where it is used today
  • What its practical advantages are

Let's dive in!


๐Ÿ“‹ The Problem Was Not โ€œValidationโ€

The application already had many small checks like ๐Ÿ‘‡

typeof value === "string";
typeof value === "number";
value === null || value === undefined;
Enter fullscreen mode Exit fullscreen mode

It also had user-defined type guards for:

  • HTTP client errors
  • Status codes
  • Literal unions
  • Arrays
  • Plain objects
  • Values coming from JSON or API responses

Each check was reasonable by itself.

The problem appeared when they started repeating.

For example, several error guards had almost the same structure.

type HttpClientError<T = unknown> = Error & {
  isHttpClientError: true;
  response?: {
    status: number;
    data: T;
  };
};

function isUnauthorizedError(error: unknown): error is HttpClientError {
  return (
    !!error &&
    (error as HttpClientError).isHttpClientError === true &&
    (error as HttpClientError).response?.status === 401
  );
}

function isValidationError(error: unknown): error is HttpClientError {
  return (
    !!error &&
    (error as HttpClientError).isHttpClientError === true &&
    (error as HttpClientError).response?.status === 422
  );
}
Enter fullscreen mode Exit fullscreen mode

This works.

But it has three practical problems:

  1. The same base check is repeated
  2. Assertion casts appear inside every guard
  3. Adding another status means adding another copy

The code was not broken.

It was simply asking for a reusable abstraction. ๐Ÿ”ง


๐Ÿƒโ€โ™‚๏ธ The Production Pattern

We replaced the repeated checks with small composable guards.

Here is a business-neutral version of the production pattern.

import { define, equalsKey, or } from "is-kit";

type HttpClientError<T = unknown> = Error & {
  isHttpClientError: true;
  code?: string;
  response?: {
    status: number;
    data: T;
  };
};

const isHttpClientError = define<HttpClientError>((value) =>
  equalsKey("isHttpClientError", true)(value),
);

const isHttpErrorWithStatus = (status: number) =>
  define<HttpClientError>(
    (value) => isHttpClientError(value) && value.response?.status === status,
  );

export const isUnauthorizedError = isHttpErrorWithStatus(401);

export const isValidationError = isHttpErrorWithStatus(422);

const hasTimeoutCode = define<HttpClientError>(
  (value) => isHttpClientError(value) && value.code === "TIMEOUT",
);

const hasTimeoutMessage = define<HttpClientError>(
  (value) => isHttpClientError(value) && value.message.includes("timed out"),
);

export const isTimeoutError = or(hasTimeoutCode, hasTimeoutMessage);
Enter fullscreen mode Exit fullscreen mode

There are a few important details here.

define

define<T> turns a runtime boolean check into a reusable predicate.

const isHttpErrorWithStatus = (status: number) =>
  define<HttpClientError>(...);
Enter fullscreen mode Exit fullscreen mode

The responsibility is still ours. The runtime check must actually prove T.

is-kit cannot make an incorrect predicate correct.

But it gives custom guards one consistent shape.

equalsKey

The base error is not plain JSON.

It is an error instance with a marker property.

So a plain-object schema is not the right abstraction here.

equalsKey("isHttpClientError", true) expresses exactly what we need,

โ€œThis value owns this key, and its value is exactly true.โ€

or

A timeout can be detected in more than one way.

Instead of creating another large conditional, we compose two reusable guards.

const isTimeoutError = or(hasTimeoutCode, hasTimeoutMessage);
Enter fullscreen mode Exit fullscreen mode

That is the core idea of is-kit,

Build small guards, then compose them.


โœจ What Actually Changed

The first adoption refactor was not just ๐Ÿ‘‡

pnpm add is-kit
Enter fullscreen mode Exit fullscreen mode

It changed the structure of the guard layer.

Observable result Change
Error guards 7 separate modules became 1 shared module
Adoption diff 335 lines added, 584 removed
Net diff 249 fewer lines
Direct imports today is-kit is isolated to 7 app helper modules
App reach today Those helpers are consumed by 39 non-test source files

The diff includes rewritten tests and helper adapters, so 249 fewer lines is not a claim that a library magically deletes code.

It is the measured result of that specific consolidation.

The more important change is the shape.

is-kit primitives
       โ†“
app guard helpers
       โ†“
features, routes, services, and UI
Enter fullscreen mode Exit fullscreen mode

The production application does not import is-kit from every component.

Instead, most call sites use application-owned helpers.


๐Ÿค” Why Keep an Application Boundary?

For primitives, the application wraps or re-exports the library guards ๐Ÿ‘‡

import {
  isNumber as isFiniteNumberGuard,
  isNumberPrimitive,
  isString as isStringGuard,
} from "is-kit";

export const isString = isStringGuard;
export const isNumber = isNumberPrimitive;
export const isFiniteNumber = isFiniteNumberGuard;
Enter fullscreen mode Exit fullscreen mode

This looks like a small detail, but it is an important design choice.

JavaScript has more than one useful meaning for โ€œnumberโ€.

typeof NaN === "number";
typeof Infinity === "number";
Enter fullscreen mode Exit fullscreen mode

In the application:

  • isNumber follows primitive typeof semantics
  • isFiniteNumber rejects NaN and Infinity

The application owns those names. is-kit provides the reusable implementation.

This boundary also means:

  • Call sites do not depend on library naming decisions
  • Semantics stay consistent across the app
  • A future migration has one clear place to start

This is how I prefer to introduce small libraries into large applications.

Adopt them behind a local vocabulary.


๐Ÿ˜Ž Other Real Usage Patterns

The HTTP error guards are the largest example, but not the only one.

Arrays

import { arrayOf, isNumberPrimitive } from "is-kit";

export const isNumberArray = arrayOf(isNumberPrimitive);
Enter fullscreen mode Exit fullscreen mode

This replaces

const isNumberArray = (value: unknown): value is number[] =>
  Array.isArray(value) &&
  value.every((item): item is number => typeof item === "number");
Enter fullscreen mode Exit fullscreen mode

Literal unions

import { oneOfValues } from "is-kit";

const VIEW_MODES = ["compact", "comfortable"] as const;

const isViewMode = oneOfValues(VIEW_MODES);

declare const input: unknown;

if (isViewMode(input)) {
  // "compact" | "comfortable"
  input;
}
Enter fullscreen mode Exit fullscreen mode

Nullish values

import { isNull, isUndefined, or } from "is-kit";

export const isNullish = or(isNull, isUndefined);
Enter fullscreen mode Exit fullscreen mode

Because this is a function, it can be reused directly.

const definedItems = items.filter((item) => !isNullish(item));
Enter fullscreen mode Exit fullscreen mode

The current application uses the same idea for:

  • Error branching
  • JSON and API-derived values
  • Filtering nullable collections
  • Literal-value checks
  • UI values that may be strings or other renderable values

This is what production usage looks like in practice.

not one giant schema,
but many small decisions at normal control-flow points.


๐Ÿš€ The Practical Advantages

After using it in the application, the advantages became clearer.

1. Incremental adoption

We did not need to redesign the data layer.

A check like

typeof value === "string";
Enter fullscreen mode Exit fullscreen mode

can become

isString(value);
Enter fullscreen mode Exit fullscreen mode

And later, if reuse becomes useful.

values.filter(isString);
Enter fullscreen mode Exit fullscreen mode

2. Less assertion casting

The old error guards repeatedly used

error as HttpClientError;
Enter fullscreen mode Exit fullscreen mode

The composed version narrows once, then accesses the narrowed value normally

isHttpClientError(value) && value.response?.status === status;
Enter fullscreen mode Exit fullscreen mode

3. Shared runtime semantics

Questions like these now have explicit answers:

  • Does โ€œnumberโ€ include NaN?
  • Does this object check accept class instances?
  • Is this field optional, nullable, or both?
  • Are two values compared with === or Object.is semantics?

The benefit is not shorter syntax alone.

It is fewer slightly-different answers across the codebase.

4. Normal TypeScript control flow

The result is still a function.

if (isValidationError(error)) {
  error.response?.data;
}
Enter fullscreen mode Exit fullscreen mode

No parse result is required.

No schema object has to travel through the application.

That makes the guards easy to use in:

  • if
  • filter
  • event handlers
  • error boundaries
  • utility functions

5. Small dependency surface

is-kit has no runtime dependencies.

That does not mean it has zero bundle cost.

It means introducing it does not bring a tree of transitive runtime packages with it.


๐Ÿ‘ฎ It Became a Team Rule

One sign of real adoption is that the library moved beyond individual preference.

The production repository now has a contributor rule:

When combining is-kit guards,
prefer or, and, andAll, nullish, and related combinators
instead of rebuilding the same composition with native operators.

For example,

const isTextOrNumber = or(isString, isNumberPrimitive);
Enter fullscreen mode Exit fullscreen mode

instead of,

const isTextOrNumber = (value: unknown) =>
  isString(value) || isNumberPrimitive(value);
Enter fullscreen mode Exit fullscreen mode

Both can return the same boolean.

But the first version is a named, reusable guard that can be passed around and composed again.

This rule is also used by coding agents working in the repository.

That matters because a tool is not truly adopted if every contributor, human or AI, invents a different style.


โœ–๏ธ What We Cannot Claim

I want to be careful here.

We did not run a controlled study showing that is-kit:

  • Improved runtime performance
  • Reduced production incidents
  • Made every validation task easier

So I will not claim those things.

The effects we can actually see are:

  • Repeated guards were consolidated
  • Assertion heavy checks became composable predicates
  • Primitive semantics became centralized
  • Application code gained reusable narrowing functions
  • The pattern became part of the repository guidelines

This is primarily a maintainability and type-safety improvement. ๐Ÿ‹๏ธโ€โ™‚๏ธ


๐Ÿ‘€ Why Not Use a Schema Library?

For these call sites, we did not need:

  • Rich validation error trees
  • Data transformations
  • A schema-first model

We needed

โ€œCan this unknown value safely enter this branch?โ€

That is exactly where a type guard fits.

For forms, API contracts, or detailed validation errors, a schema library such as Zod may still be the better tool.

They solve different problems.


๐ŸŽฏ What 50 Stars Means to Me

50 stars is small compared with the largest TypeScript libraries.

But OSS does not become meaningful only after thousands of stars.

For me, this milestone means:

  • People outside the project understand the idea
  • The API is useful beyond a toy example
  • The library is solving a real maintenance problem
  • There is still a lot to improve

And the production application gives the milestone some weight.

is-kit is not only being starred.

It is currently helping real application code answer:

โ€œWhat is this value, and can TypeScript trust it?โ€

Thank you to everyone who starred, tested, reported an issue, or simply looked at the repository.

If small composable type guards fit your TypeScript style, give it a try ๐Ÿ‘‡

GitHub logo nyaomaru / is-kit

Build small guards. Compose them. Lightweight, zero-dependency TypeScript type guards for runtime validation and natural narrowing. Runtime-safe ๐Ÿ›ก๏ธ, composable ๐Ÿงฉ, and ergonomic โœจ.

is-kit

is-kit logo

npm version JSR npm downloads License

Build small guards. Compose them.

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 unknown values without a large schema framework

๐Ÿ“š Documentation Site

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 isX functions instead of one-off inline checks
  • keep runtime validation lightweight and dependency-free
  • narrow values directly in if, filter, and other TypeScriptโ€ฆ




Top comments (0)