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

> Source: <https://dev.to/nyaomaru/is-kit-reached-100-stars-heres-what-changed-since-50-49pf>
> Published: 2026-10-21 15:08:25+00:00

Hoi hoi!

YAY! [`is-kit`](https://github.com/nyaomaru/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`](https://github.com/Effect-TS/effect).

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

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

At 50 stars, the biggest thing I wanted to show was that [`is-kit`](https://github.com/nyaomaru/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

``` js
declare const input: unknown;
```

Maybe I already know how to validate a string

``` js
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.**

``` js
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.

``` js
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 narrowing`Error` brand checks`struct` 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`](https://github.com/nyaomaru/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 👇

``` js
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 👇

``` js
(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

``` js
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)](https://bsky.app/profile/did:plc:uloc6bgqlunyjpqh2odxrqar?ref_src=embed) [2026-10-15T16:38:16.787Z](https://bsky.app/profile/did:plc:uloc6bgqlunyjpqh2odxrqar/post/3mxwiqfeo3s2j?ref_src=embed)

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! 🤲
