0

Xử lý `unknown` trong TypeScript có phải quá phiền phức không?

Xin chào mọi người! 👋

Mình là một frontend engineer đang sống tại Hà Lan. Gần đây mình cũng đang phải vật lộn với mùa dị ứng phấn hoa 😿

Dữ liệu trả về từ API, input từ form, thông tin đến từ các dịch vụ bên ngoài...

Trong TypeScript, chúng ta thường xuyên phải xử lý các giá trị có kiểu unknown.

Tuy nhiên, việc kiểm tra và xử lý chúng một cách an toàn đôi khi lại trở thành một công việc khá phiền phức trong quá trình phát triển hằng ngày.

Lực hấp dẫn của unknown

Đúng vậy, unknown dường như có một lực hấp dẫn mạnh mẽ như cả vũ trụ.

Dù vậy, chúng ta vẫn muốn xử lý type một cách an toàn, đúng không?

Vì lý do đó, mình đã tạo ra is-kit, một thư viện nhẹ giúp xây dựng và kết hợp các Type Guard.

is-kit

is-kit là gì?

is-kit là một toolkit nhẹ, không có dependency, dùng để xây dựng các Type Guard có thể tái sử dụng trong TypeScript.

Nó giúp bạn viết những hàm nhỏ như isFoo, kết hợp chúng thành các kiểm tra runtime phức tạp hơn và giữ cho quá trình type narrowing diễn ra tự nhiên trong flow thông thường của ứng dụng.

Mục tiêu của is-kit là cung cấp các kiểm tra runtime:

  • An toàn
  • Có thể kết hợp
  • Dễ sử dụng
  • Không có dependency

mà không bắt buộc bạn phải sử dụng một workflow nặng dựa trên schema.

Với is-kit, bạn có thể:

  • Tạo và tái sử dụng các Type Guard có type rõ ràng
  • Kết hợp các Guard bằng and, or, notoneOf
  • Kiểm tra cấu trúc object và collection
  • Xử lý giá trị unknown mà không cần một schema framework lớn
  • Assert hoặc refine các giá trị trong runtime

📚 Tài liệu của is-kit

is-kit đặc biệt phù hợp với type narrowing bên trong ứng dụng, lọc dữ liệu và xây dựng các Guard có thể tái sử dụng.

🤔 Tại sao nên sử dụng is-kit?

Bạn có cảm thấy mệt mỏi vì phải viết đi viết lại cùng một điều kiện isFoo không?

is-kit có thể phù hợp khi bạn muốn:

  • Tạo các hàm isX có thể tái sử dụng thay vì viết các điều kiện dùng một lần
  • Giữ runtime validation nhẹ và không có dependency
  • Narrow type trực tiếp trong if, filter và các flow TypeScript thông thường
  • Kết hợp nhiều quy tắc kiểm tra nhỏ với nhau

Các thư viện như Zod sử dụng cách tiếp cận tập trung vào schema.

Ngược lại, is-kit tập trung vào việc refine type ngay bên trong code hiện có của ứng dụng.

Thay vì nghĩ rằng mình đang “viết một schema validation”, bạn có thể xem is-kit như một cách thêm type safety vào các câu lệnh if hằng ngày.

Nếu Zod đặc biệt hữu ích ở boundary của ứng dụng, chẳng hạn như:

  • API response
  • Form input
  • Dữ liệu bên ngoài

thì is-kit tập trung nhiều hơn vào logic bên trong ứng dụng.


Một ví dụ đơn giản

Giả sử bạn cần kiểm tra nhiều lần xem một giá trị có phải là string dài tối đa ba ký tự hay không.

Với is-kit, bạn có thể định nghĩa điều kiện đó một lần và tái sử dụng:

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

const isShortString = define<string>(
  (value) => isString(value) && value.length <= 3,
);

Sau đó, bạn có thể sử dụng nó như một hàm thông thường:

import { isShortString } from "~/utils/is";

declare const input: unknown;

// Trước đây: phải lặp lại điều kiện mỗi lần
if (typeof input === "string" && input.length <= 3) {
  input.toUpperCase();
}

// Sau đó: tái sử dụng Type Guard
if (isShortString(input)) {
  input.toUpperCase();
}

Cách viết này hoạt động tự nhiên với:

  • if
  • filter
  • map
  • Những flow logic khác đã có trong ứng dụng

Bạn không cần thay đổi toàn bộ cấu trúc code.


🐾 is-kit đã phát triển như thế nào?

Đã khoảng sáu tháng kể từ khi phiên bản v1.0 được phát hành.

Ban đầu, is-kit chỉ là một thư viện Type Guard nhỏ với một số chức năng cơ bản như:

  • define
  • and
  • or
  • struct
  • arrayOf

Kể từ đó, cho đến phiên bản v1.6, nó đã dần phát triển thành một công cụ thực tế hơn:

Một toolkit dùng để xử lý các giá trị unknown trong ứng dụng TypeScript thực tế.

Sau đây là năm cải tiến đáng chú ý.


🪄 1. Phân biệt property không tồn tại và property có giá trị undefined

Trong API response, chúng ta thường gặp hai trường hợp:

  • Property hoàn toàn không tồn tại trong object
  • Property tồn tại nhưng giá trị là undefined

Hai trường hợp này không giống nhau.

Trong phiên bản v1.5.0, optionalKey(...) đã được thêm vào:

import { isString, optional, optionalKey, struct } from "is-kit";

const isUser = struct({
  id: isString,
  nickname: optionalKey(isString),
  displayName: optionalKey(optional(isString)),
});

Điều này cho phép chúng ta mô tả rõ ràng hai cấu trúc khác nhau.

Trong ví dụ trên:

  • nickname có thể không tồn tại, nhưng nếu tồn tại thì phải là string
  • displayName có thể không tồn tại, hoặc có thể tồn tại với giá trị undefined

Nhờ đó, cấu trúc object có thể được biểu diễn chính xác hơn.


🔑 2. Type narrowing dựa trên property

Từ phiên bản v1.1.13 đến v1.4.0, một số utility dùng để làm việc với property cụ thể đã được thêm vào:

  • hasKey
  • hasKeys
  • narrowKeyTo

Ví dụ:

import {
  hasKeys,
  narrowKeyTo,
  oneOfValues,
  struct,
  isString,
  isNumber,
} from "is-kit";

const isUser = struct({
  id: isString,
  age: isNumber,
  role: oneOfValues("admin", "guest", "trial"),
});

const hasRoleAndId = hasKeys("role", "id");

const byRole = narrowKeyTo(isUser, "role");

const isGuest = byRole("guest");

Những utility này cho phép bạn tạo Guard mới từ Guard hiện có mà không cần định nghĩa lại toàn bộ cấu trúc object.


🧪 3. Sử dụng assert cho flow Fail Fast

assert được thêm vào trong phiên bản v1.2.0:

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

declare const input: unknown;

assert(isString, input, "input must be a string");

input.toUpperCase();

Sau khi assert chạy thành công, TypeScript hiểu rằng input là một string.

Nhờ đó, Type Guard cũng có thể được sử dụng trong các flow validation theo phong cách Fail Fast.


✨ 4. Hỗ trợ SetMap

Trong phiên bản v1.6.0, is-kit đã bổ sung hỗ trợ cho SetMap:

import { mapOf, setOf, isString, isNumber } from "is-kit";

const isTags = setOf(isString);

const isScores = mapOf(isString, isNumber);

Dữ liệu trong ứng dụng thực tế không phải lúc nào cũng là array.

Việc hỗ trợ các cấu trúc này giúp is-kit có thể kiểm tra thêm nhiều collection phổ biến trong JavaScript.


🥏 5. Xử lý các trường hợp đặc biệt của number

Từ các phiên bản v1.1.x, is-kit đã bổ sung nhiều Guard liên quan đến number:

  • isInteger
  • isSafeInteger
  • isPositive
  • isNegative
  • isNaN
  • isInfiniteNumber
  • isZero

Những Guard này giúp chúng ta mô tả chính xác hơn:

Trong context hiện tại, một number hợp lệ thực sự là gì?

Trong nhiều trường hợp, chỉ kiểm tra:

typeof value === "number"

là chưa đủ.

Ví dụ:

  • NaN cũng có type là number
  • Infinity cũng có type là number
  • Một số trường hợp chỉ chấp nhận integer
  • Một số trường hợp chỉ chấp nhận số dương

Sử dụng Guard cụ thể hơn sẽ giúp ý định của code trở nên rõ ràng hơn.


🌟 Các chức năng khác

is-kit còn cung cấp nhiều utility khác.

Bạn có thể xem toàn bộ chức năng trong tài liệu:

📚 Tài liệu của is-kit


🎯 Tổng kết

is-kit bắt đầu như một thư viện nhỏ dùng để kết hợp các Type Guard.

Qua nhiều phiên bản, nó đã dần phát triển thành:

Một toolkit thực tế để xử lý các giá trị unknown trong logic của ứng dụng TypeScript.

Những cải tiến chính bao gồm:

  • Mô tả cấu trúc object chính xác hơn bằng optionalKey
  • Type narrowing dựa trên property
  • Fail Fast với assert
  • Hỗ trợ SetMap
  • Các Guard chính xác hơn dành cho number

Mục tiêu của is-kit rất đơn giản:

Giúp code type-safe cũng trở nên tự nhiên và dễ viết.

Nếu bạn thử dự án và có bất kỳ ý tưởng hoặc phản hồi nào, hãy chia sẻ với mình nhé!

Hẹn gặp lại trong bài viết tiếp theo 👋

Xem is-kit trên GitHub


Bài viết gốc bằng tiếng Anh:

https://dev.to/nyaomaru/handling-unknown-in-typescript-isnt-it-painful-4dec


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í