0

is-kit đạt 100 stars ⭐🎉 Có gì thay đổi kể từ mốc 50?

Hoi hoi!

YAY! is-kit vừa đạt 100 GitHub stars⭐ 🎉🎉🎉

Khi đạt 50 stars, mình đã viết về cách chúng mình sử dụng thư viện trong một ứng dụng TypeScript production phục vụ hơn 100.000 người dùng. 👇

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

Mình có thể kể lại câu chuyện đó. Nhưng như vậy thì chán quá. 😹

Rất nhiều chuyện đã xảy ra từ đó đến nay.

Thư viện có những API mới, một số API cũ trở nên chặt chẽ hơn, mình bắt đầu dùng nó trong nhiều dự án OSS của chính mình, và một trường hợp sử dụng thực tế thậm chí còn làm lộ ra một edge case về suy luận kiểu trong Effect.

Vậy nên lần này, thay vì nói lại chuyện áp dụng vào production, mình muốn chia sẻ:

  • Những thay đổi từ 50 đến 100 stars
  • Một số API quan trọng nhất được bổ sung
  • Cách mình thật sự sử dụng is-kit ngoài ứng dụng production ban đầu
  • Một báo cáo bug thực tế liên quan đến Effect.catchIf
  • 100 stars có ý nghĩa gì với mình ở thời điểm hiện tại

Bắt đầu nhé! 🔧

is-kit 100 stars


⭐ Trước tiên, 50 stars có ý nghĩa gì?

Ở mốc 50 stars, điều mình muốn cho mọi người thấy nhất là is-kit không chỉ là một dự án làm cho vui.

Nó đã được dùng trong một ứng dụng production thực tế. 🚀

Ứng dụng có những runtime check lặp đi lặp lại như:

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

và nhiều user-defined type guards phức tạp hơn dành cho HTTP errors, literal unions, arrays, API values và plain objects.

Điểm thú vị không phải là is-kit làm những câu lệnh kiểm tra này ngắn hơn. Điểm thú vị là nó giúp chúng có một cấu trúc có thể tái sử dụng.

Các guard nhỏ có thể được kết hợp với nhau.

Những helper thuộc về ứng dụng có thể nằm giữa thư viện và feature code, trong khi kết quả vẫn hoạt động với control flow TypeScript bình thường.

Điều đó không thay đổi.

Nhưng từ mốc 50 stars, bản thân thư viện đã tiến khá xa. 👀


🔑 Property refinement trở thành một use case chính thức

Có một nhu cầu mà mình liên tục gặp phải:

Mình đã có một type guard tốt rồi. Mình chỉ muốn áp dụng nó cho một property.

Ví dụ:

declare const input: unknown;

Có thể mình đã biết cách kiểm tra một chuỗi:

import { isString } from "is-kit";

Nhưng giờ mình muốn diễn đạt rằng:

“Object này có property name, và property đó là string.”

Nghe đơn giản, nhưng thực ra có vài trường hợp khác nhau.

Property bắt buộc khác property tùy chọn. Array index khác object property.

Và một optional property đã được xác nhận có giá trị phải được narrow khác với property vẫn còn optional.

Vì thế, is-kit bổ sung ba property refinement helpers:

  • refineKey
  • refineDefinedKey
  • refineIndex

Ý tưởng rất đơn giản:

Lấy một type guard bạn đã tin tưởng và áp dụng nó lên property hoặc phần tử array.

Ví dụ:

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

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

Giờ đây string guard có thể được tái sử dụng ở cấp sâu hơn, thay vì phải viết lại trong một custom predicate khác.

API này nhỏ thôi, nhưng mình thích vì nó đi theo cùng design goal với phần còn lại của is-kit.

Đừng xây một validator khổng lồ.

Hãy xây những check nhỏ có thể tái sử dụng trong TypeScript bình thường.


🔍 Rồi discriminatedUnion được nâng cấp đáng kể

Bổ sung lớn nhất kể từ bài viết 50 stars có lẽ là exhaustive discriminated union API được giới thiệu ở v1.15.0.

Hãy xem một kiểu TypeScript thường gặp:

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

Ở runtime, việc validate nó không khó.

Chúng ta có thể kiểm tra ok, rồi validate các trường tương ứng.

Nhưng có một câu hỏi khác:

TypeScript có thể kiểm tra chính validator đã bao phủ toàn bộ union chưa?

Đây là lúc discriminatedUnion phát huy tác dụng.

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 behavior rất hữu ích.

Nhưng phần thú vị hơn là compile-time contract.

Builder kiểm tra những thứ như:

  • Thiếu discriminant branches
  • Thừa branches
  • Branch guards không tương thích
  • Discriminant tuples chưa đầy đủ

Vì thế, nếu union thay đổi, guard không thể âm thầm trở nên thiếu sót.

Điều này quan trọng vì một trong những cách type guard viết tay trở nên nguy hiểm nhất chính là drift.

TypeScript type thay đổi. Runtime guard thì không. Và compiler thường không thể biết rằng hai thứ đã không còn đồng bộ.

discriminatedUnion là một nỗ lực làm cho kiểu sai sót đó khó xảy ra hơn.


💪 Thư viện cũng trở nên chặt chẽ hơn

Không phải mọi cải tiến từ 50 đến 100 stars đều là feature mới.

Rất nhiều công sức được dành cho việc làm hành vi hiện có chính xác hơn.

Ví dụ, những release gần đây đã sửa các vấn đề liên quan đến:

  • Literal-key narrowing của recordOf
  • Kiểm tra native Error brand
  • Array guards với iterator bị override
  • Exact struct validation
  • Struct schemas dùng symbol keys
  • Cross-realm struct schemas

Package-level testing cũng được mở rộng.

Giờ is-kit kiểm tra hành vi package đã publish từ góc nhìn TypeScript consumer dùng cả ESM lẫn CommonJS, và có smoke coverage cho Deno / JSR.

Đây không phải thứ hấp dẫn nhất để trình bày trong một code snippet.

Nhưng với một thư viện nhỏ, mình thấy nó rất quan trọng.

Đảm bảo mọi thứ chạy đúng trong những môi trường JavaScript và TypeScript kỳ lạ mới chính là thử thách thực tế.


🤖 Mình cũng bắt đầu dùng is-kit trong các dự án OSS khác

Từ mốc 50 stars, mình bắt đầu sử dụng is-kit một cách tự nhiên hơn trong những dự án OSS khác mà mình duy trì.

Ví dụ, trong công cụ changelog của changelog-bot, mình dùng define để giữ các predicate nhỏ có thể tái sử dụng và kết hợp, thay vì biến mỗi runtime check thành một helper riêng lẻ.

Một ví dụ đơn giản hóa như sau 👇

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

Đoạn code chính xác không phải điều quan trọng.

Điều thay đổi ở mình là không còn nghĩ:

“Mình nên dùng thư viện của mình ở đây.”

Mình chỉ đơn giản lấy guard mình cần.

Một thư viện bắt đầu thật sự hữu ích với chính maintainer khi nó không còn có cảm giác như một bản demo của bản thân nó.


🚀 Một guard của is-kit gặp phải bug kiểu dữ liệu trong Effect

Đây là một trong những chuyện thú vị nhất gần đây.

Một người dùng is-kit sử dụng type guard với catchIf của Effect.

Về mặt ý tưởng, refinement trông như thế này 👇

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

Điều đó hoàn toàn bình thường đối với is-kit.

Thực tế, unknown chính là điểm bắt đầu mà thư viện được thiết kế để xử lý.

Nhưng với Effect.catchIf, refinement đó có thể ảnh hưởng đến generic type inference theo cách không mong muốn.

Error channel có thể bị widen thành unknown.

Trường hợp được báo cáo đã dẫn đến PR này của Effect 👏 Và PR đã được merge! 🚀

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

Kiểu dữ liệu liên quan ban đầu gần như thế này:

refinement: Predicate.Refinement<E, EB>;

Và bản sửa đổi thành:

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

Ý tưởng chính là input type của refinement không được phép định nghĩa lại error type đã được Effect xác lập trước đó.

PR cũng bổ sung type tests cho những trường hợp như:

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

Trước bản sửa, input của refinement có thể tham gia inference và làm widen error channel.

Với NoInfer, error type ban đầu vẫn là nguồn sự thật.

Cùng một pattern cũng được sửa cho Channel và Stream.

Ngoài ra còn có bài đăng Bluesky về vấn đề này:

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

Mình rất thích bug này vì nó xuất phát từ một tương tác mà có lẽ mình không bao giờ tìm ra nếu chỉ test riêng is-kit.

is-kit không “làm hỏng Effect”. Và Effect cũng không làm điều gì vô lý.

Đó là một tương tác rất tinh tế trong TypeScript inference giữa generic API và refinement có input cố ý được khai báo rộng.

Đây chính xác là loại edge case xuất hiện khi các thư viện bắt đầu gặp nhau trong code thực tế.


👍 Đây chính là kiểu feedback mình mong muốn

Khi is-kit đạt 50 stars, mình từng viết:

“Có ai đó thấy ý tưởng này hữu ích.”

Chỉ vậy thôi cũng đã đủ khiến mình vui rồi.

100 stars vẫn không có nghĩa đây là một thư viện khổng lồ.

Star không phải benchmark. Nó cũng không chứng minh tính đúng đắn. Và chắc chắn không có nghĩa mọi dự án TypeScript đều nên cài is-kit.

Nhưng có điều đã thay đổi.

Mọi người không chỉ star repository nữa.

Họ đang:

  • Sử dụng thư viện
  • Đặt câu hỏi
  • Tìm ra edge cases
  • Báo bugs
  • So sánh với những cách validation khác
  • Dùng guards cùng các thư viện TypeScript khác
  • Khiến mình phải suy nghĩ lại về những API tưởng đã hoàn thiện

Điều đó có giá trị hơn con số rất nhiều. 💖


💬 Dự án cũng trở nên dễ giải thích hơn

Một điều khác cũng thay đổi từ 50 đến 100 stars.

Mình hiểu rõ hơn is-kit không phải là gì.

Nó không cố thay thế mọi schema library. Nó không cố đưa mọi runtime value vào một schema-first workflow. Và nó không cố che giấu control flow bình thường của TypeScript.

Câu hỏi cốt lõi vẫn rất nhỏ:

“Giá trị unknown này có thể đi vào branch này một cách an toàn không?”

Đôi khi schema library là lựa chọn đúng.

Ví dụ nếu mình cần:

  • Validation errors phong phú
  • Transformations
  • Form validation
  • Contract dùng chung giữa các hệ thống

Mình có thể chọn Zod, Valibot, ArkType hoặc một thư viện khác.

Nhưng đôi khi mình đã có code TypeScript thông thường và chỉ cần một narrowing function có thể tái sử dụng.

Đó là nơi mình muốn is-kit hữu ích.

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

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

items.filter(isString);

result.catch(isHttpClientError);

const isMaybeNumber = nullish(isNumber);

Những quyết định runtime nhỏ. Những function bình thường. TypeScript bình thường.


🐱 100 stars có ý nghĩa gì với mình?

100 stars vẫn còn nhỏ so với những dự án TypeScript OSS lớn nhất.

Không sao cả.

Điều quan trọng là dự án đang trở nên thực tế hơn theo những cách không liên quan đến bộ đếm stars.

Kể từ cột mốc 50 stars:

  • API mới được bổ sung
  • API hiện có trở nên chặt chẽ hơn
  • Các runtime edge cases được sửa
  • Package compatibility testing được cải thiện
  • Mình bắt đầu dùng is-kit một cách tự nhiên trong OSS khác
  • Việc sử dụng bên ngoài mang lại feedback mà mình không thể tự tạo ra
  • Một refinement dựa trên unknown thậm chí giúp phát hiện inference edge case trong Effect

Điều cuối cùng có lẽ là điều mình thích nhất.

Bởi mục tiêu chưa bao giờ là xây một thư viện chỉ trông đẹp trong README của chính nó!!!

Mục tiêu là để runtime narrowing có thể tái sử dụng hoạt động tự nhiên trong code TypeScript thực tế.

Và giờ mình bắt đầu thấy điều đó xảy ra cả bên ngoài repository của mình.

Cảm ơn tất cả mọi người đã star, thử dùng, báo lỗi, đặt câu hỏi, mở issue, hay chỉ đơn giản đọc code. 🙏

Vẫn còn rất nhiều điều mình muốn cải thiện.

Nhưng mốc 100 thực sự rất vui. 😸⭐

Nếu những type guard nhỏ, có thể kết hợp phù hợp với phong cách TypeScript của bạn, hãy thử nhé!

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

Cảm ơn đã đọc! 🤲


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí