cd /news/developer-tools/is-kit-reached-100-stars-heres-what-… · home › topics › developer-tools › article
[ARTICLE · art-154636] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

is-kit Reached 100 Stars ⭐🎉 Here’s What Changed Since 50

A developer's open-source TypeScript type-guard library, is-kit, has reached 100 GitHub stars, up from 50, after adding property refinement helpers (refineKey, refineDefinedKey, refineIndex) and an exhaustive discriminated-union API in v1.15.0. The library, used in a production TypeScript application serving over 100,000 users, also surfaced a type-inference edge case in Effect-TS's Effect library through a real-world use case.

by read9 min views2 publishedOct 21, 2026

Hoi hoi!

YAY! is-kit just reached 100 GitHub stars⭐ 🎉🎉🎉

When it reached 50 stars, I wrote about how we use it in a production TypeScript application serving more than 100,000 users. 👇

I could write the same story again. But that would be boring. 😹

A lot has happened since then.

The library gained new APIs, some existing APIs became stricter, I started using it in more of my own OSS projects, and one real-world use case even exposed a type inference edge case in Effect.

So this time, instead of explaining the production adoption again, I want to look at:

is-kit outside the original production applicationEffect.catchIf Let's dive in! 🔧

At 50 stars, the biggest thing I wanted to show was that is-kit was not only a toy project.

It was already being used in a real production application. 🚀

The application had repeated runtime checks like

typeof value === "string";
typeof value === "number";
value === null || value === undefined;

and several larger user-defined type guards for things like HTTP errors, literal unions, arrays, API values, and plain objects.

The interesting part was not that is-kit made those checks shorter.

The interesting part was that it gave them one reusable shape.

Small guards could be composed.

Application-owned helpers could sit between the library and feature code. And the result could still participate in normal TypeScript control flow.

That part has not changed.

But since 50 stars, the library itself has moved quite a bit. 👀

One thing I kept running into was this.

I already have a good type guard.

I just want to apply it to one property.

For example

declare const input: unknown;

Maybe I already know how to validate a string

import { isString } from "is-kit";

But now I want to express

"This object has a name property, and that property is a string."

That sounds simple, but there are actually a few different cases.

A required property is not the same as an optional property.

An array index is not the same as an object property.

And an optional property that becomes known to be defined should narrow differently from one that is still optional.

So is-kit added three property refinement helpers:

refineKey`` refineDefinedKey``refineIndex The idea is simple

Take a type guard you already trust and lift it onto a property or array element.

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

const hasStringName = refineKey("name", isString);

Now the string guard is reusable at a deeper level instead of being rewritten inside another custom predicate.

This is a small API, but I like it because it follows the same design goal as the rest of is-kit.

Do not build one giant validator.

Build small checks that can be reused in normal TypeScript code.

discriminatedUnion Got Much More Serious The biggest addition since the 50-star article is probably the exhaustive discriminated union API introduced in v1.15.0.

Consider a common TypeScript type

type Result = { ok: true; value: string } | { ok: false; error: string };

At runtime, validating this is not difficult.

You can check ok, then validate the corresponding fields.

But there is another question

Can TypeScript verify that the validator itself covers the whole union?

That is where discriminatedUnion comes in.

import { discriminatedUnion, isString, oneOfValues, typedStruct } from "is-kit";

type Result = { ok: true; value: string } | { ok: false; error: string };

const resultUnion = discriminatedUnion<Result>();

export const isResult = resultUnion("ok", [true, false], {
  true: typedStruct<Extract<Result, { ok: true }>>()({
    ok: oneOfValues(true),
    value: isString,
  }),
  false: typedStruct<Extract<Result, { ok: false }>>()({
    ok: oneOfValues(false),
    error: isString,
  }),
});

The runtime behavior is useful.

But the more interesting part is the compile-time contract.

The builder checks things like:

So if the union changes, the guard cannot silently stay incomplete.

That matters because one of the easiest ways for a hand-written type guard to become dangerous is drift.

The TypeScript type evolves.

The runtime guard does not.

And the compiler cannot normally tell you that the two are no longer aligned.

discriminatedUnion is one attempt to make that class of mistake harder.

Not every improvement between 50 and 100 stars was a new feature.

A lot of work was about making existing behavior more precise.

For example, recent releases included fixes around:

recordOf literal-key narrowingError brand checksstruct validation There was also more package-level testing.

is-kit now checks published-package behavior from both ESM and CommonJS TypeScript consumers, and also has Deno / JSR smoke coverage.

This part is less exciting to show in a code snippet.

But for a small library, I think it is important.

Making sure they behave correctly in weird JavaScript and TypeScript environments is where things become real.

Since reaching 50 stars, I've also started using is-kit more naturally in other OSS projects I maintain.

For example, in my changelog tooling of changelog-bot, I use define to keep small predicates reusable and composable instead of turning every runtime check into an isolated helper.

A simplified example looks like this 👇

import { define, oneOfValues } from "is-kit";

const CHANGELOG_SECTIONS = ["Added", "Changed", "Fixed", "Removed"] as const;

const isChangelogSection = define<(typeof CHANGELOG_SECTIONS)[number]>(
  oneOfValues(CHANGELOG_SECTIONS),
);

The exact code is not the important part.

What changed for me is that I no longer think

"I should use my library here."

I just reach for the guard I need.

A library starts becoming useful to its maintainer when it stops feeling like a demo of itself.

This was one of the most interesting things that happened recently.

An is-kit user was using a type guard with Effect's catchIf.

Conceptually, the refinement looked like this 👇

(error: unknown): error is SomeError => ...

That is completely normal for is-kit.

In fact, unknown is exactly where the library is designed to start.

But with Effect.catchIf, that refinement could affect generic inference in an unexpected way.

The error channel could become widened to unknown.

The reported case resulted in this Effect PR 👏 And merged 🚀

When passing (arg: unknown) => arg is SomeError to some catchIf s, it turned the error channel into unknown. This PR makes only self's Error slot influence the types, regardless of the type taken by refinement.

Validation: the Channel, Effect, and Stream type-test files pass on TypeScript 5.9.3 and 6.0.3, and pnpm check passes. Temporarily reverting the implementation reproduces unknown error-channel widening in Channel data-last usage and in unrelated-refinement cases across all three APIs. The matching data-first refinement cases also pass without the fix. No tests were changed during maintainer validation.

Closes EFF-2233

The relevant type originally looked roughly like this

refinement: Predicate.Refinement<E, EB>;

and the fix changed it to

refinement: Predicate.Refinement<NoInfer<E>, EB>;

The important idea is that the refinement's input type should not be allowed to redefine the error type already established by the Effect.

The PR also adds type tests for cases like

const result = Effect.catchIf(
  mixedEffect,
  (e: unknown): e is AiError => e instanceof AiError,
  () => Effect.succeed("ok"),
);

Without the fix, the refinement input could participate in inference and widen the error channel.

With NoInfer, the original error type remains the source of truth.

The same pattern was fixed for Channel and Stream too.

There was also a Bluesky post about the issue

Not like the lib itself caught it, but when I created a reusable operator: export const isNotFoundError = and( isObject, refineDefinedKey('reason', equalsKey('_tag', 'NotFound')), ) I noticed my streams having unknown in the error channel github.com/Effect-TS/ef...

—Evangeline Rome 🌵🦎🌱🐢🐍🐸 🥦 (@evadeva.bsky.social) 2026-10-15T16:38:16.787Z

I really liked this bug because it came from an interaction I would probably never have discovered by testing is-kit alone.

is-kit did not "break Effect". And Effect was not doing something absurd either.

It was a subtle TypeScript inference interaction between a generic API and a refinement whose input was intentionally broad.

That is exactly the kind of edge case that appears when libraries start meeting each other in real code.

When is-kit reached 50 stars, I wrote

"Someone found this idea useful."

That was already enough to make me happy.

100 stars still does not mean the library is huge.

A star is still not a benchmark.

It is still not proof of correctness.

And it definitely does not mean every TypeScript project should install is-kit.

But something has changed.

People are not only starring the repository anymore.

They are

That is much more valuable than the number itself. 💖

Another thing changed between 50 and 100 stars.

I became much clearer about what is-kit is not.

It is not trying to replace every schema library.

It is not trying to turn every runtime value into a schema-first workflow.

And it is not trying to hide normal TypeScript control flow.

The core question is still very small

"Can this unknown value safely enter this branch?"

Sometimes a schema library is the right answer.

For example, if I need:

I may want Zod, Valibot, ArkType, or something else.

But sometimes I already have normal TypeScript code and I only need a reusable narrowing function.

That is where I want is-kit to be useful.

if (isUser(value)) {
  value.id;
}

if (isHttpClientError(error)) {
  // Handle HTTP client errors
}

items.filter(isString);

result.catch(isHttpClientError);

const isMaybeNumber = nullish(isNumber);

Small runtime decisions.

Normal functions.

Normal TypeScript.

100 stars is still small compared with the largest TypeScript OSS projects.

No problem.

What matters to me is that the project is becoming more real in ways that have nothing to do with the star counter.

Since the 50-star milestone:

unknown-based refinement even helped expose an inference edge case in Effect That last part is probably my favorite.

Because the goal was never to build a library that only looks nice in its own README!!!

The goal is to make reusable runtime narrowing work naturally inside real TypeScript code.

And now I am starting to see that happen outside my own repository too.

Thank you to everyone who starred the project, tried it, reported something, asked a question, opened an issue, or simply read the code. 🙏

There is still a lot I want to improve.

But 100 feels pretty nice. 😸⭐

If small composable type guards fit your TypeScript style, give it a try!

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.

and, or, not, oneOf Best…

Thank you for reading! 🤲

── more in #developer-tools 4 stories · sorted by recency
── more on @is-kit 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/is-kit-reached-100-s…] indexed:0 read:9min 2026-10-21 · —