Hoi hoi!

YAY!is-kit 剛剛達成了 100 個 GitHub stars⭐ 🎉🎉🎉

當它達到 50 顆星時,我寫過一篇關於我們如何在一個服務超過 100,000 名使用者的正式 TypeScript 應用程式中使用它的文章。👇

https://dev.to/nyaomaru/is-kit-reached-50-stars-heres-how-we-use-it-in-production-2i5b

我本來可以再寫一次同樣的故事,但那樣就太無聊了。😹

從那之後發生了很多事。

這個函式庫新增了新 API,一些既有 API 也變得更嚴格,我開始在更多自己的 OSS 專案中自然地使用它,而且一個真實世界的使用案例甚至揭露了 Effect 裡的一個型別推斷邊界案例。

所以這次,我不想再重複解釋一次正式上線的故事,而是想看看:

  • 50 星到 100 星之間有哪些變化
  • 這段時間新增了哪些最重要的 API
  • 我現在在原本的正式應用程式之外,實際是怎麼使用 is-kit 的
  • 一個涉及 Effect.catchIf 的真實 bug 回報
  • 100 顆星對我現在代表什麼

讓我們開始吧!🔧

is-kit 100 stars


⭐ 先說說,50 顆星代表什麼?

在 50 顆星的時候,我最想表達的是:is-kit 不只是一個玩具專案。

它當時已經被用在一個真正的正式環境應用程式裡了。🚀

那個應用程式裡有很多重複的執行期檢查,例如

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

以及一些更大型、使用者自訂的型別守衛,用於處理 HTTP 錯誤、字面值聯合型別、陣列、API 值、以及純物件等情境。

有趣的地方不是 is-kit 讓這些檢查變短了。
有趣的地方是,它替這些檢查提供了一種可重複使用的形狀。

小型守衛可以互相組合。

應用程式自有的輔助函式可以位於函式庫與功能程式碼之間,而結果仍然可以參與正常的 TypeScript 控制流程。

這一點沒有改變。

但從 50 顆星之後,這個函式庫本身其實已經進化了不少。👀


🔑 屬性細化變成了第一級用途

我一直反覆遇到的一件事是這樣:

我已經有一個很好的型別守衛了。
我只是想把它套用在某個屬性上。

例如

declare const input: unknown;

也許我已經知道怎麼驗證字串了

import { isString } from "is-kit";

但現在我想表達的是

「這個物件有一個 name 屬性,而且那個屬性是一個字串。」

聽起來很簡單,但其實有幾種不同的情況。

必要屬性不等於可選屬性。
陣列索引不等於物件屬性。

而且,原本是可選的屬性,一旦被確認存在之後,應該要以不同方式縮窄,而不是跟仍然可選的情況一樣。

所以 is-kit 新增了三個屬性細化工具:

  • refineKey
  • refineDefinedKey
  • refineIndex

概念很簡單:

把你已經信任的型別守衛,提升到某個屬性或陣列元素上使用。

例如

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

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

現在,字串守衛就可以在更深一層重複使用,而不用在另一個自訂判斷式裡再寫一次。

這是一個小小的 API,但我喜歡它,因為它延續了 is-kit 一貫的設計目標。

不要做一個超大的驗證器。

要做的是可以在一般 TypeScript 程式碼中重複使用的小型檢查。


🔍 接著,discriminatedUnion 變得重要多了

從 50 顆星文章之後,最大的新增功能大概就是在 v1.15.0 引入的完整枚舉聯合型別 API。

考慮一個常見的 TypeScript 型別:

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

在執行期驗證這個型別並不難。

你可以先檢查 ok,再驗證對應欄位。

但還有另一個問題:

TypeScript 能不能驗證這個驗證器本身有覆蓋到整個聯合型別?

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

執行期行為很有用。

但更有意思的是編譯期契約。

這個 builder 會檢查像這些情況:

  • 遺漏聯合分支
  • 多出來的分支
  • 不相容的分支守衛
  • 不完整的判別值元組

所以如果聯合型別改變了,這個守衛就不能默默地維持不完整。

這很重要,因為手寫型別守衛最容易變危險的原因之一,就是漂移。

TypeScript 型別變了。
執行期守衛沒變。
而編譯器通常無法告訴你,這兩者已經不再對齊了。

discriminatedUnion 就是希望讓這類錯誤更難發生的一種嘗試。


💪 這個函式庫也變得更嚴格了

50 到 100 顆星之間的改進,不全都是新功能。

很多工作其實是在讓既有行為更精確。

例如,最近幾個版本包含了以下修正:

  • recordOf 字面值鍵縮窄
  • 原生 Error 品牌檢查
  • 覆寫迭代器的陣列守衛
  • 精確的 struct 驗證
  • 以 symbol 為鍵的 struct schema
  • 跨執行環境(cross-realm)的 struct schema

另外也增加了更多套件層級的測試。

is-kit 現在會同時檢查從 ESM 與 CommonJS TypeScript 消費者角度來看的已發佈套件行為,也加入了 Deno / JSR 的煙霧測試覆蓋。

這部分不像程式碼片段那麼吸睛。

但對一個小型函式庫來說,我認為這很重要。

確保它們在奇怪的 JavaScript 和 TypeScript 環境下仍能正確運作,這才是真正開始面對現實的地方。


🤖 我也開始把 is-kit 用在其他 OSS 專案裡了

自從達到 50 顆星之後,我也開始更自然地把 is-kit 用在我維護的其他 OSS 專案中。

例如,在我的 changelog-bot 變更記錄工具裡,我使用 define 來讓小型判斷式保持可重用與可組合,而不是把每個執行期檢查都變成孤立的 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),
);

重點不是這段程式碼本身。

對我來說,改變的是我不再會想著

「我應該在這裡用我的函式庫。」

我只是直接拿我需要的守衛來用。

當一個函式庫讓維護者不再覺得它是在展示自己,而是自然地成為工具時,它才真的開始變得有用。


🚀 接著,一個 is-kit 守衛碰上了 Effect 的型別 bug

這是最近發生過最有趣的事情之一。

有位 is-kit 使用者把一個型別守衛用在 Effect 的 catchIf 上。

概念上,這個細化大概像這樣👇

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

這對 is-kit 來說完全正常。

事實上,unknown 正是這個函式庫預期的起點。

但在 Effect.catchIf 裡,這個細化可能會以意想不到的方式影響泛型推斷。

錯誤通道可能會被擴展成 unknown。

這個回報最後促成了 Effect 的這個 PR 👏,而且已經合併 🚀

https://github.com/Effect-TS/effect/pull/9252

相關型別原本大致長這樣:

refinement: Predicate.Refinement<E, EB>;

而修正後改成:

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

重點概念是:細化的輸入型別不應該反過來重新定義已經由 Effect 建立好的錯誤型別。

這個 PR 也為像這樣的情況加入了型別測試:

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

如果沒有這個修正,細化的輸入就可能參與推斷,進而擴展錯誤通道。

有了 NoInfer 之後,原本的錯誤型別就仍然是事實來源。

Channel 和 Stream 也用了同樣的修正模式。

此外,關於這個問題還有一篇 Bluesky 貼文:

https://bsky.app/profile/evadeva.bsky.social/post/3mxwiqfeo3s2j

我很喜歡這個 bug,因為它來自一種我大概不可能只靠測試 is-kit 自己就發現的互動。

is-kit 並沒有「破壞 Effect」,Effect 也沒有做什麼離譜的事。

這只是泛型 API 與一個刻意設計成輸入範圍很廣的細化之間,出現了微妙的 TypeScript 推斷互動。

這正是當不同函式庫開始在真實程式碼中彼此相遇時,才會浮現出來的那種邊界案例。


👍 這就是我想要的回饋類型

當 is-kit 到了 50 顆星時,我寫道:

「有人覺得這個想法有用。」

光是這樣就已經足夠讓我開心了。

100 顆星仍然不代表這個函式庫很大。

星星不是效能基準。
它也不是正確性的證明。
更不代表每個 TypeScript 專案都應該安裝 is-kit。

但有些事已經改變了。

大家不再只是收藏這個儲存庫而已。

他們開始:

  • 使用這個函式庫
  • 提出問題
  • 發現邊界案例
  • 回報 bug
  • 拿它跟其他驗證方案比較
  • 把它的守衛和其他 TypeScript 函式庫一起用
  • 讓我重新思考我以為已經完成的 API

這比數字本身有價值得多。💖


💬 這個專案也變得更容易解釋了

50 到 100 顆星之間,還有一件事也變了。

我對 is-kit 不是什麼 變得更清楚了。

它不是要取代所有 schema 函式庫。
它不是要把每個執行期值都變成 schema-first 工作流程。
它不是要隱藏正常的 TypeScript 控制流程。

核心問題依然很小:

「這個 unknown 值能安全地進入這個分支嗎?」

有時候 schema 函式庫才是正確答案。

例如,如果我需要:

  • 豐富的驗證錯誤
  • 轉換
  • 表單驗證
  • 跨系統共享的契約

我可能會想用 Zod、Valibot、ArkType,或其他工具。

但有時候我已經有正常的 TypeScript 程式碼,而我只需要一個可重用的縮窄函式。

那就是我希望 is-kit 發揮作用的地方。

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

if (isHttpClientError(error)) {
  // 處理 HTTP 用戶端錯誤
}

items.filter(isString);

result.catch(isHttpClientError);

const isMaybeNumber = nullish(isNumber);

小型的執行期判斷。
一般的函式。
正常的 TypeScript。


🐱 100 顆星對我來說代表什麼

和最大的 TypeScript OSS 專案相比,100 顆星還是很小。

沒關係。

對我來說,重要的是:這個專案正在以一些與星數完全無關的方式,變得越來越真實。

從 50 顆星這個里程碑之後:

  • 新 API 被加入了
  • 既有 API 變得更嚴格
  • 執行期邊界案例被修正了
  • 套件相容性測試變好了
  • 我開始在其他 OSS 中自然地使用 is-kit
  • 外部使用帶來了我自己無法單獨產生的回饋
  • 一個基於 unknown 的細化甚至幫助揭露了 Effect 裡的推斷邊界案例

最後這一點大概是我最喜歡的。

因為目標從來不是做一個只能在自己的 README 裡看起來很好看的函式庫!!!

目標是讓可重用的執行期縮窄,在真實的 TypeScript 程式碼中自然運作。

而現在,我開始在自己倉庫之外看到這件事真的發生了。

謝謝所有收藏這個專案、試用它、回報問題、提出疑問、開 issue,或只是閱讀程式碼的人。🙏

我還有很多想改進的地方。

但 100 顆星真的很不錯。😸⭐

如果小而可組合的型別守衛很符合你的 TypeScript 風格,歡迎試試看!

https://github.com/nyaomaru/is-kit

感謝閱讀!🤲


原文出處:https://dev.to/nyaomaru/is-kit-reached-100-stars-heres-what-changed-since-50-49pf


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝9   💬1  
308
🥈
mike
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登