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 裡的一個型別推斷邊界案例。
所以這次,我不想再重複解釋一次正式上線的故事,而是想看看:
is-kit 的Effect.catchIf 的真實 bug 回報讓我們開始吧!🔧

在 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 新增了三個屬性細化工具:
refineKeyrefineDefinedKeyrefineIndex概念很簡單:
把你已經信任的型別守衛,提升到某個屬性或陣列元素上使用。
例如
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 schemastruct 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。
但有些事已經改變了。
大家不再只是收藏這個儲存庫而已。
他們開始:
這比數字本身有價值得多。💖
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。
和最大的 TypeScript OSS 專案相比,100 顆星還是很小。
沒關係。
對我來說,重要的是:這個專案正在以一些與星數完全無關的方式,變得越來越真實。
從 50 顆星這個里程碑之後:
is-kitunknown 的細化甚至幫助揭露了 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