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

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

1 1 17
calendar_today agoschedule7 min read
— Originally published at dev.to

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

Image description

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;

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

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

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

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

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

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

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;

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

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

This replaces

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

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

Nullish values

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

export const isNullish = or(isNull, isUndefined);

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

const definedItems = items.filter((item) => !isNullish(item));

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

can become

isString(value);

And later, if reuse becomes useful.

values.filter(isString);

2. Less assertion casting

The old error guards repeatedly used

error as HttpClientError;

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

isHttpClientError(value) && value.response?.status === status;

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

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

instead of,

const isTextOrNumber = (value: unknown) =>
  isString(value) || isNumberPrimitive(value);

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

{% embed https://github.com/nyaomaru/is-kit %}

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

The Sovereign Vault — A Comprehensive Guide to Protocol-Driven AI

Ken W. Algerverified - Jun 4

5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)

Dharanidharan - Mar 3
chevron_left
348 Points19 Badges
Heerlen, the Netherlandsnyaomaru-portfolio.vercel.app
9Posts
4Comments
5Connections
Funny Frontend Engineer! ? Living in the Netherlands.

Related Jobs

View all jobs →

Commenters (This Week)

5 comments
3 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!