is-kit 达成 100 Stars ⭐🎉 从 50 到 100,发生了什么?

0 阅读9分钟

Hoi hoi!

好耶!is-kit 刚刚达成了 100 个 GitHub Stars⭐ 🎉🎉🎉

达到 50 Stars 时,我写过一篇文章,介绍我们如何在服务超过 10 万用户的生产级 TypeScript 应用中使用它。👇

dev.to/nyaomaru/is…

我本可以再讲一遍同样的故事,但那就太无聊了。😹

从那以后发生了不少事情。

库增加了新的 API,一些现有 API 变得更加严格,我开始在自己的其他 OSS 项目中使用它,甚至有一个真实使用场景暴露了 Effect 中的 TypeScript 类型推断边界问题。

所以这次不再重复介绍生产环境中的应用,而是想聊聊:

  • 从 50 到 100 Stars 有什么变化
  • 期间增加的几个重要 API
  • 我如何在最初的生产应用之外使用 is-kit
  • 一个涉及 Effect.catchIf 的真实 bug 报告
  • 现在 100 Stars 对我意味着什么

开始吧!🔧

is-kit 100 stars


⭐ 首先,50 Stars 意味着什么?

达到 50 Stars 时,我最想说明的是,is-kit 并不是一个仅供演示的玩具项目。

它已经被用于真实的生产环境。🚀

应用中存在许多重复的 runtime 检查,比如:

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

还有一些更复杂的自定义 type guards,用来检查 HTTP 错误、字面量联合类型、数组、API 数据和普通对象等。

有意思的地方不在于 is-kit 让这些检查变短。 而在于它为这些检查提供了一种可复用的形式。

小型 guards 可以组合起来使用。

应用自己的 helpers 可以位于库和业务代码之间,而最终结果仍然能配合正常的 TypeScript 控制流工作。

这一点没有改变。

但从 50 Stars 到现在,库本身已经前进了不少。👀


🔑 属性细化成为正式支持的使用场景

我经常遇到这样一种需求:

我已经有一个不错的 type guard。 我只是想把它应用到某个属性上。

例如:

declare const input: unknown;

也许我已经知道怎么验证字符串:

import { isString } from "is-kit";

但现在我想表达:

“这个对象有一个 name 属性,而且它的值是字符串。”

听起来简单,但实际上有几种不同情况。

必需属性不等于可选属性。 数组索引不等于对象属性。

而且,一个已被确认有值的可选属性,应该和一个仍然可选的属性产生不同的类型收窄结果。

因此,is-kit 增加了三个属性细化 helpers:

  • refineKey
  • refineDefinedKey
  • refineIndex

思路很简单:

拿一个已经可信的 type guard,把它应用到对象属性或数组元素上。

例如:

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

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

这样,已有的字符串 guard 就能在更深层复用,而不必在另一个自定义 predicate 里重新实现一遍。

这个 API 很小,但我很喜欢,因为它延续了 is-kit 的设计目标。

不去构建一个巨型 validator。

而是构建能够在普通 TypeScript 代码中复用的小型检查。


🔍 然后,discriminatedUnion 变得更加强大

自 50 Stars 那篇文章以来,最大的新增功能可能就是 v1.15.0 引入的穷尽式 discriminated union API。

考虑一个常见的 TypeScript 类型:

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

在 runtime 验证它其实不难。

检查 ok,再验证相应字段即可。

但还有另一个问题:

TypeScript 能否验证 validator 本身覆盖了整个 union?

这就是 discriminatedUnion 的用武之地。

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

它在 runtime 的行为很实用。

但更有意思的是 compile-time contract。

这个 builder 会检查:

  • 缺失的 discriminant 分支
  • 多余分支
  • 与分支不兼容的 guards
  • 不完整的 discriminant tuples

因此,如果 union 发生变化,guard 就不能悄悄保持不完整。

这一点很重要,因为手写 type guard 最容易出问题的原因之一就是 drift(类型和检查逻辑逐渐脱节)。

TypeScript 类型在演进。 runtime guard 却没跟上。 而通常编译器并不知道两者已经不再一致。

discriminatedUnion 正是希望让这类错误更难发生。


💪 库本身也变得更加严格

50 到 100 Stars 之间的改进并不全是新功能。

很多工作是为了让已有行为更加精确。

例如,近期版本修复了:

  • recordOf 对字面量 key 的类型收窄
  • 原生 Error 的 brand 检查
  • iterator 被覆写的数组 guards
  • struct 的 exact validation
  • 使用 symbol key 的 struct schemas
  • 跨 realm 的 struct schemas

package 层面的测试也增加了。

现在 is-kit 会从 ESM 和 CommonJS TypeScript consumers 的角度检查发布后的 package,还覆盖了 Deno / JSR smoke tests。

这些东西放在代码片段里可能没那么吸引眼球。

但我认为这对小型库十分重要。

真正的考验,是它们在各种奇怪的 JavaScript 和 TypeScript 环境中能否正确工作。


🤖 我开始在其他 OSS 项目里使用 is-kit

达到 50 Stars 后,我也越来越自然地在自己维护的其他 OSS 项目中使用 is-kit。

比如,在 changelog-bot 的 changelog 工具代码里,我会使用 define 来让小型 predicates 保持可复用、可组合,而不是把每次 runtime 检查都写成孤立的 helper。

一个简化的例子如下 👇

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

具体代码并不是重点。

对我而言真正的变化是,我已经不再想着:

“这里应该使用我自己的库。”

而是自然而然地拿起需要的 guard。

当一个库不再让维护者觉得自己只是在演示它,而是能直接解决问题时,它才开始真正变得有用。


🚀 一个 is-kit Guard 意外遇到了 Effect 的类型问题

这是最近发生的事情中最有意思的一件。

一位 is-kit 用户把 type guard 和 Effect 的 catchIf 一起使用。

概念上,这个 refinement 大致是这样的 👇

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

对于 is-kit 来说,这完全正常。

实际上,unknown 恰恰是这个库最初设计时考虑的起点。

但在 Effect.catchIf 中,这个 refinement 可能意外影响泛型类型推断。

error channel 可能被拓宽为 unknown。

这个报告最终促成了下面这个 Effect PR 👏 而且已经合并!🚀

github.com/Effect-TS/e…

相关类型原本大致是:

refinement: Predicate.Refinement<E, EB>;

修复后改成:

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

关键在于:refinement 的输入类型不应该重新定义已经由 Effect 确定的错误类型。

该 PR 还为下面这样的情况增加了类型测试:

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

修复之前,refinement 的输入可能参与类型推断,从而把 error channel 拓宽。

使用 NoInfer 后,原本的错误类型仍然是唯一可信的依据。

同样的模式也在 Channel 和 Stream 中得到了修复。

关于这个问题,还有一篇 Bluesky 帖子:

bsky.app/profile/eva…

我特别喜欢这个 bug,因为如果只测试 is-kit 本身,我大概永远不会发现这种交互问题。

不是 is-kit “弄坏了 Effect”,也不是 Effect 做了什么荒谬的事情。

它是一个微妙的 TypeScript 类型推断交互:一边是泛型 API,另一边是输入类型刻意写得很宽泛的 refinement。

这正是不同库开始在真实代码里相遇时才会出现的 edge case。


👍 这正是我想收到的反馈

is-kit 达到 50 Stars 时,我写过:

“有人觉得这个想法有用。”

光是这样就已经让我很开心了。

100 Stars 依然不意味着这个库有多庞大。

Star 不是 benchmark。 也不是正确性的证明。 更不代表所有 TypeScript 项目都应该安装 is-kit。

但有些东西确实变了。

人们不再只是给 repository 点个 Star。

他们开始:

  • 真正使用这个库
  • 提出问题
  • 发现 edge cases
  • 报告 bugs
  • 与其他验证方案进行比较
  • 把 guards 用在其他 TypeScript 库中
  • 让我重新考虑那些自认为已经完成的 API

这些远比数字本身更有价值。💖


💬 这个项目也越来越容易解释了

从 50 到 100 Stars,还有另一个变化。

我越来越清楚 is-kit 不是什么。

它不是想取代所有 schema libraries。 它不是想把每个 runtime value 都变成 schema-first workflow。 它也不是想隐藏 TypeScript 正常的控制流。

核心问题仍然非常简单:

“这个 unknown 值能安全地进入这个分支吗?”

有时 schema library 才是正确的选择。

例如,当我需要:

  • 丰富的验证错误信息
  • Transformations
  • 表单验证
  • 跨系统共享的 contract

我可能会选择 Zod、Valibot、ArkType 或其他工具。

但有时我已经在写普通 TypeScript 代码,只需要一个可复用的 narrowing function。

这就是我希望 is-kit 发挥作用的地方。

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

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

items.filter(isString);

result.catch(isHttpClientError);

const isMaybeNumber = nullish(isNumber);

小小的 runtime 决策。 普通的函数。 普通的 TypeScript。


🐱 100 Stars 对我意味着什么

和那些最大的 TypeScript OSS 项目相比,100 Stars 依然不多。

没关系。

对我而言,重要的是项目正以一些与 Star 数量完全无关的方式变得更加真实。

自从达到 50 Stars:

  • 增加了新 API
  • 现有 API 变得更加严格
  • 修复了 runtime edge cases
  • 改进了 package 兼容性测试
  • 我开始自然地在其他 OSS 中使用 is-kit
  • 外部实际使用带来了我自己无法创造的反馈
  • 一个基于 unknown 的 refinement 甚至帮助暴露了 Effect 的推断边界问题

最后这一点大概是我最喜欢的。

因为我的目标从来不是写一个只在自己 README 里看起来很漂亮的库!!!

真正的目标是:让可复用的 runtime narrowing 自然地融入真实的 TypeScript 代码。

而现在,我开始看到它在自己的 repository 之外发生了。

感谢每一位点 Star、尝试、反馈、提问、创建 issue,或者只是读过代码的人。🙏

我还有很多想改进的地方。

不过,100 Stars 的感觉还是很棒。😸⭐

如果小巧、可组合的 type guards 适合你的 TypeScript 风格,欢迎试试看!

github.com/nyaomaru/is…

感谢阅读!🤲