{"slug": "is-kit-reached-100-stars-heres-what-changed-since-50", "title": "is-kit Reached 100 Stars ⭐🎉 Here’s What Changed Since 50", "summary": "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.", "body_md": "Hoi hoi!\n\nYAY! [`is-kit`](https://github.com/nyaomaru/is-kit) just reached **100 GitHub stars⭐** 🎉🎉🎉\n\nWhen it reached 50 stars, I wrote about how we use it in a production TypeScript application serving more than 100,000 users. 👇\n\nI could write the same story again. But that would be boring. 😹\n\nA lot has happened since then.\n\nThe 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).\n\nSo this time, instead of explaining the production adoption again, I want to look at:\n\n`is-kit` outside the original production application`Effect.catchIf`\nLet's dive in! 🔧\n\nAt 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.\n\nIt was already being used in a real production application. 🚀\n\nThe application had repeated runtime checks like\n\n```\ntypeof value === \"string\";\ntypeof value === \"number\";\nvalue === null || value === undefined;\n```\n\nand several larger user-defined type guards for things like HTTP errors, literal unions, arrays, API values, and plain objects.\n\nThe interesting part was **not** that `is-kit` made those checks shorter.\n\nThe interesting part was that it gave them one **reusable shape**.\n\nSmall guards could be composed.\n\nApplication-owned helpers could sit between the library and feature code. And the result could still participate in normal TypeScript control flow.\n\nThat part has not changed.\n\nBut since 50 stars, the library itself has moved quite a bit. 👀\n\nOne thing I kept running into was this.\n\nI already have a good type guard.\n\nI just want to apply it to one property.\n\nFor example\n\n``` js\ndeclare const input: unknown;\n```\n\nMaybe I already know how to validate a string\n\n``` js\nimport { isString } from \"is-kit\";\n```\n\nBut now I want to express\n\n\"This object has a `name` property, and that property is a string.\"\n\nThat sounds simple, but there are actually a few different cases.\n\nA required property is **not the same as an optional property**.\n\nAn array index is **not the same as an object property**.\n\nAnd an optional property that becomes known to be defined should **narrow differently from one that is still optional**.\n\nSo `is-kit` added three property refinement helpers:\n\n`refineKey`` refineDefinedKey``refineIndex`\nThe idea is simple\n\n**Take a type guard you already trust and lift it onto a property or array element.**\n\n``` js\nimport { isString, refineKey } from \"is-kit\";\n\nconst hasStringName = refineKey(\"name\", isString);\n```\n\nNow the string guard is reusable at a deeper level instead of being rewritten inside another custom predicate.\n\nThis is a small API, but I like it because it follows the same design goal as the rest of `is-kit`.\n\nDo not build one giant validator.\n\n**Build small checks that can be reused in normal TypeScript code**.\n\n`discriminatedUnion` Got Much More Serious\nThe biggest addition since the 50-star article is probably the exhaustive discriminated union API introduced in `v1.15.0`.\n\nConsider a common TypeScript type\n\n```\ntype Result = { ok: true; value: string } | { ok: false; error: string };\n```\n\nAt runtime, validating this is not difficult.\n\nYou can check `ok`, then validate the corresponding fields.\n\nBut there is another question\n\nCan TypeScript verify that the validator itself covers the whole union?\n\nThat is where `discriminatedUnion` comes in.\n\n``` js\nimport { discriminatedUnion, isString, oneOfValues, typedStruct } from \"is-kit\";\n\ntype Result = { ok: true; value: string } | { ok: false; error: string };\n\nconst resultUnion = discriminatedUnion<Result>();\n\nexport const isResult = resultUnion(\"ok\", [true, false], {\n  true: typedStruct<Extract<Result, { ok: true }>>()({\n    ok: oneOfValues(true),\n    value: isString,\n  }),\n  false: typedStruct<Extract<Result, { ok: false }>>()({\n    ok: oneOfValues(false),\n    error: isString,\n  }),\n});\n```\n\nThe runtime behavior is useful.\n\nBut the more interesting part is the compile-time contract.\n\nThe builder checks things like:\n\nSo if the union changes, the guard cannot silently stay incomplete.\n\nThat matters because one of the easiest ways for a hand-written type guard to become dangerous is drift.\n\nThe TypeScript type evolves.\n\nThe runtime guard does not.\n\nAnd the compiler cannot normally tell you that the two are no longer aligned.\n\n`discriminatedUnion` is one attempt to make that class of mistake harder.\n\nNot every improvement between 50 and 100 stars was a new feature.\n\nA lot of work was about making existing behavior more precise.\n\nFor example, recent releases included fixes around:\n\n`recordOf` literal-key narrowing`Error` brand checks`struct` validation\nThere was also more package-level testing.\n\n`is-kit` now checks published-package behavior from both ESM and CommonJS TypeScript consumers, and also has `Deno` / `JSR` smoke coverage.\n\nThis part is less exciting to show in a code snippet.\n\nBut for a small library, I think it is important.\n\nMaking sure they behave correctly in weird JavaScript and TypeScript environments is where things become real.\n\nSince reaching 50 stars, I've also started using `is-kit` more naturally in other OSS projects I maintain.\n\nFor 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.\n\nA simplified example looks like this 👇\n\n``` js\nimport { define, oneOfValues } from \"is-kit\";\n\nconst CHANGELOG_SECTIONS = [\"Added\", \"Changed\", \"Fixed\", \"Removed\"] as const;\n\nconst isChangelogSection = define<(typeof CHANGELOG_SECTIONS)[number]>(\n  oneOfValues(CHANGELOG_SECTIONS),\n);\n```\n\nThe exact code is not the important part.\n\nWhat changed for me is that I no longer think\n\n\"I should use my library here.\"\n\nI just reach for the guard I need.\n\nA library starts becoming useful to its maintainer when it stops feeling like a demo of itself.\n\nThis was one of the most interesting things that happened recently.\n\nAn `is-kit` user was using a type guard with Effect's `catchIf`.\n\nConceptually, the refinement looked like this 👇\n\n``` js\n(error: unknown): error is SomeError => ...\n```\n\nThat is completely normal for `is-kit`.\n\nIn fact, `unknown` is exactly where the library is designed to start.\n\nBut with `Effect.catchIf`, that refinement could affect generic inference in an unexpected way.\n\nThe error channel could become widened to `unknown`.\n\nThe reported case resulted in this Effect PR 👏 And merged 🚀\n\nWhen 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.\n\nValidation: 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.\n\nCloses EFF-2233\n\nThe relevant type originally looked roughly like this\n\n```\nrefinement: Predicate.Refinement<E, EB>;\n```\n\nand the fix changed it to\n\n```\nrefinement: Predicate.Refinement<NoInfer<E>, EB>;\n```\n\nThe important idea is that the **refinement's input type should not be allowed to redefine the error type already established by the `Effect`**.\n\nThe PR also adds type tests for cases like\n\n``` js\nconst result = Effect.catchIf(\n  mixedEffect,\n  (e: unknown): e is AiError => e instanceof AiError,\n  () => Effect.succeed(\"ok\"),\n);\n```\n\nWithout the fix, the refinement input could participate in inference and widen the error channel.\n\nWith `NoInfer`, the original error type remains the source of truth.\n\nThe same pattern was fixed for `Channel` and `Stream` too.\n\nThere was also a `Bluesky` post about the issue\n\nNot 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...\n\n—[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)\n\nI really liked this bug because **it came from an interaction I would probably never have discovered by testing `is-kit` alone**.\n\n`is-kit` did not \"break Effect\". And `Effect` was not doing something absurd either.\n\nIt was a subtle TypeScript inference interaction between a generic API and a refinement whose input was intentionally broad.\n\nThat is exactly the kind of edge case that appears when libraries start meeting each other in real code.\n\nWhen `is-kit` reached 50 stars, I wrote\n\n\"Someone found this idea useful.\"\n\nThat was already enough to make me happy.\n\n100 stars still does not mean the library is huge.\n\nA star is still not a benchmark.\n\nIt is still not proof of correctness.\n\nAnd it definitely does not mean every TypeScript project should install `is-kit`.\n\nBut something has changed.\n\nPeople are not only starring the repository anymore.\n\nThey are\n\nThat is much more valuable than the number itself. 💖\n\nAnother thing changed between 50 and 100 stars.\n\nI became much clearer about what `is-kit` is **not**.\n\nIt is **not** trying to replace every schema library.\n\nIt is **not** trying to turn every runtime value into a schema-first workflow.\n\nAnd it is **not** trying to hide normal TypeScript control flow.\n\nThe core question is still very small\n\n**\"Can this `unknown` value safely enter this branch?\"**\n\nSometimes a schema library is the right answer.\n\nFor example, if I need:\n\nI may want `Zod`, `Valibot`, `ArkType`, or something else.\n\nBut sometimes I already have normal TypeScript code and I only need a reusable narrowing function.\n\nThat is where I want `is-kit` to be useful.\n\n```\nif (isUser(value)) {\n  value.id;\n}\n\nif (isHttpClientError(error)) {\n  // Handle HTTP client errors\n}\n\nitems.filter(isString);\n\nresult.catch(isHttpClientError);\n\nconst isMaybeNumber = nullish(isNumber);\n```\n\nSmall runtime decisions.\n\nNormal functions.\n\nNormal TypeScript.\n\n100 stars is still small compared with the largest TypeScript OSS projects.\n\nNo problem.\n\nWhat matters to me is that the project is becoming more real in ways that have nothing to do with the star counter.\n\nSince the 50-star milestone:\n\n`unknown`-based refinement even helped expose an inference edge case in Effect\nThat last part is probably my favorite.\n\nBecause the goal was never to build a library that only looks nice in its own `README`!!!\n\nThe goal is **to make reusable runtime narrowing work naturally inside real TypeScript code**.\n\nAnd now I am starting to see that happen outside my own repository too.\n\nThank you to everyone who starred the project, tried it, reported something, asked a question, opened an issue, or simply read the code. 🙏\n\nThere is still a lot I want to improve.\n\nBut 100 feels pretty nice. 😸⭐\n\nIf small composable type guards fit your TypeScript style, give it a try!\n\n`is-kit` is a lightweight, zero-dependency toolkit for building reusable TypeScript **type guards**.\n\n`is-kit` is not just a collection of `isX` helpers. Its main focus is composing\nsmall runtime checks into reusable guards while preserving useful TypeScript\nnarrowing.\n\nIt helps you write small `isFoo` functions, compose them into **richer runtime\nchecks**, refine properties on values you already know about, and keep\n**TypeScript narrowing** natural inside regular control flow. Use it at runtime\nboundaries when needed, without requiring a schema-first workflow.\n\n**Runtime-safe** 🛡️, **composable** 🧩, and **ergonomic** ✨ without asking you to adopt a heavy schema workflow.\n\n`and`, `or`, `not`, `oneOf`\nBest…\n\nThank you for reading! 🤲", "url": "https://wpnews.pro/news/is-kit-reached-100-stars-heres-what-changed-since-50", "canonical_source": "https://dev.to/nyaomaru/is-kit-reached-100-stars-heres-what-changed-since-50-49pf", "published_at": "2026-10-21 15:08:25+00:00", "updated_at": "2026-10-21 15:10:01.058331+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["is-kit", "Effect", "TypeScript", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/is-kit-reached-100-stars-heres-what-changed-since-50", "markdown": "https://wpnews.pro/news/is-kit-reached-100-stars-heres-what-changed-since-50.md", "text": "https://wpnews.pro/news/is-kit-reached-100-stars-heres-what-changed-since-50.txt", "jsonld": "https://wpnews.pro/news/is-kit-reached-100-stars-heres-what-changed-since-50.jsonld"}}