close

DEV Community

Cover image for is-kit Reached 50 Stars ⭐ Here’s How We Use It in Production
nyaomaru
nyaomaru

Posted on Edited on

is-kit Reached 50 Stars ⭐ Here’s How We Use It in Production

Real-world scale for type safety

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 · 🧭 Practical Guides

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

Top comments (14)

Collapse
 
mattewens profile image
mattewens

Congrats! 50 stars on a type-utility lib is nothing to sniff at, that space is crowded. The bit that jumped out for me was the "is it worth the extra runtime cost" section, because that's the exact question I keep dodging on my own micro-libraries and then paying for later in a hot loop.

Do you have a rough sense of how much of the adoption came from the OSS side vs people finding it via your posts here?

Collapse
 
nyaomaru profile image
nyaomaru

Thanks a lot! 🙌

I haven’t tracked every adoption case yet, so I can’t give a complete breakdown.

One confirmed case is a company that already knew me and adopted is-kit in a real production application. That’s the production usage I wrote about in the article.

As for OSS adoption, I haven’t confirmed any public usage yet.

So at the moment, the clearest confirmed adoption I can point to is the production project from the article. 😸

I’d love to get a better picture of OSS adoption as the project grows. 👍

Collapse
 
mattewens profile image
mattewens

Really appreciate the honest breakdown - most people wave the star count around and skip the "actual production usage" question entirely.

Same pattern showed up when I was shipping my voice-agent boilerplate on
GitHub: first 5 real users were all people I'd DM'd or talked to on Discord, and the "cold OSS" adoption curve was basically flat for two months. What eventually broke it was writing the exact kind of teardown posts you're doing here - walking through why the utility mattered in a specific painful bug, not just "look what I built".

If you're up for it, tags like #typescript #productivity #cleancode seem to have decent staying power on this platform. #typescript alone sends me about 30% of my article traffic. Might help is-kit find the
cold-adoption crowd that isn't on your Twitter feed already.

Thread Thread
 
nyaomaru profile image
nyaomaru

Thanks, this is really helpful! 🚀

I think the question of “what pain made this OSS necessary?” doesn’t only exist when the library is created. New pain points also become visible after adoption, so I want to keep asking myself what kinds of problems is-kit can actually solve in real projects. 👀

And thanks for the tag suggestions too! I haven’t really used #productivity or #cleancode before, but they seem like a great fit for is-kit.

I’ll definitely keep them in mind for future posts on X and other platforms.

Really appreciate you sharing your experience and the traffic insight! 🙌

Thread Thread
 
mattewens profile image
mattewens

That post-adoption pain thing is such a good frame - I noticed the same pattern on my voice-agent boilerplate. The bug reports from months 2 and 3 were completely different from the initial "getting started" friction, and honestly the more interesting problems only emerged once people started building weird stuff on top of it.

Chasing those is where the real product-market fit lives, I think.

Thread Thread
 
nyaomaru profile image
nyaomaru

That makes a lot of sense. 😸

It’s a great example of how the assumptions we make before real adoption can be very different from how people actually use the tool in practice.

That’s really valuable advice. I want to keep listening closely to users, learn from those unexpected use cases, and continue improving is-kit.

Thanks again! 👍

Collapse
 
effnd profile image
Marat Sabitov

For me personally, production usage example is an order of magnitude more significant than GitHub stars or NPM downloads. And I especially appreciate zero-dependency approach — it helps avoid pulling things you'll possibly never use into the build. I wish your project further development and advancement!🚀

Collapse
 
nyaomaru profile image
nyaomaru

Thanks for the kind words! 😸
I completely agree, seeing is-kit actually used in production feels much more meaningful than just watching the numbers grow.

At the same time, it also made me realize that I need to take maintenance and long-term support even more seriously.
I’ll keep developing and improving is-kit. Thanks again for your support! 🚀

Collapse
 
hadil profile image
Hadil Ben Abdallah

Congratulations, nyaomaru! 🎉 Great job!
Keep going! 🙌🏻
My star in the 60th 😎

Collapse
 
nyaomaru profile image
nyaomaru

Thank you so much!! 😺
I’m really happy you became the 60th star ⭐ That means a lot to me!

I’ll keep working on is-kit and making it better. Thanks again for your support! 🙌🏻🚀

Collapse
 
rizzdev profile image
Andrew R

Fifty stars is not a reliability signal. Push one level deeper on packaging and ask whether published entrypoints, types, and peer ranges match what production actually imports. That gap is where open source green-lights fail in real apps

Collapse
 
nyaomaru profile image
nyaomaru

I agree! 😺
I see stars more as a signal of interest or popularity than reliability.

For packaging, is-kit has a package-level smoke test that packs the actual npm artifact, installs it into a temporary consumer project, and verifies ESM, CJS, and TypeScript imports.

That said, there’s still a lot to learn from how people actually use the library in production. I’d like to keep supporting adoption, talking with users, and using that feedback to brush up the package over time. 💪

Collapse
 
ajmal_ca profile image
Ajmal C A

Yeah, it’s a small Node package, but it makes developers’ work easier, reduces code, and is easier to maintain.

Collapse
 
nyaomaru profile image
nyaomaru

Thank you for the kind words! That really means a lot to me 😸 I’ll keep working hard to make is-kit even better!