0

Vì sao `filter(Boolean)` không phải là bộ lọc Nullish trong TypeScript 🔧

Xin chào! 👋

Mình là @nyaomaru, một Frontend Engineer và cũng đang xây dựng một vài dự án OSS nhỏ bằng TypeScript. 😸

Hôm nay mình muốn nói về một dòng JavaScript rất ngắn và rất quen thuộc:

values.filter(Boolean);

Có lẽ bạn đã thấy cách viết này rất nhiều lần.

  • Ngắn gọn
  • Tiện
  • Và đôi khi, nó đúng chính xác với điều bạn muốn

Nhưng nếu ý định thực sự của bạn là:

Chỉ loại bỏ nullundefined

thì filter(Boolean) đang làm nhiều hơn thế.

Hãy xem vì sao nhé! 👀


🕳️ Cái bẫy nhỏ của filter(Boolean)

Giả sử chúng ta có một array như sau:

const values = ["ready", "", 0, false, null, undefined];

Bây giờ ta muốn loại bỏ những giá trị “không có”:

const result = values.filter(Boolean);

Ở runtime, kết quả còn lại là gì?

["ready"];

Khoan đã.

Ta chỉ muốn loại bỏ:

null;
undefined;

Nhưng đồng thời ta cũng mất luôn:

"";
0;
false;

Tại sao?

Boolean sẽ chuyển giá trị được truyền vào thành boolean và chỉ giữ lại những giá trị có kết quả là true.

Nó không hề kiểm tra:

Giá trị này có phải là null hoặc undefined không?


🤔 Falsy và Nullish không phải là một

JavaScript coi tất cả những giá trị dưới đây là falsy:

false;
0;
"";
null;
undefined;
NaN;

Nhưng hai yêu cầu sau hoàn toàn khác nhau:

Loại bỏ tất cả giá trị falsy

và:

Chỉ loại bỏ nullundefined

Trong ứng dụng thực tế, 0, false"" hoàn toàn có thể là dữ liệu hợp lệ.

Ví dụ:

type Settings = {
  retryCount: number;
  notificationsEnabled: boolean;
  nickname: string;
};

Tất cả những giá trị sau đều có thể hợp lệ:

retryCount = 0;
notificationsEnabled = false;
nickname = "";

Falsy không đồng nghĩa với “missing”.


🧠 Trong TypeScript cũng có một khác biệt

filter(Boolean) loại bỏ các giá trị falsy ở runtime, nhưng Boolean không phải là một type guard.

Vì vậy, TypeScript thường không thể kết luận rằng nullundefined đã được loại bỏ chỉ vì bạn dùng filter(Boolean).

const values: Array<
  string | number | boolean | null | undefined
> = [
  "ready",
  "",
  0,
  false,
  null,
  undefined,
];

const result = values.filter(Boolean);

// result:
// Array<string | number | boolean | null | undefined>

Vì thế, nếu rule ở runtime của bạn thực sự là:

Chỉ giữ lại các giá trị truthy

thì filter(Boolean) hoàn toàn ổn.

Nhưng nó không thể hiện rõ ý định:

Chỉ loại bỏ các giá trị nullish

Điều này đúng cả với người đọc code lẫn type system của TypeScript.


✅ Hãy viết đúng rule mà bạn thực sự muốn

Nếu rule của bạn là:

Giữ lại mọi thứ ngoại trừ nullundefined

thì hãy viết đúng như vậy:

const values: Array<string | null | undefined> = [
  "Ada",
  null,
  "Linus",
  undefined,
];

const names = values.filter(
  (value): value is string =>
    value !== null && value !== undefined,
);

Bây giờ:

// names: string[]

Đây là TypeScript hoàn toàn bình thường.

Nếu check này chỉ xuất hiện một lần, bạn không cần thêm library nào cả.


⚠️ Một edge case của browser: cẩn thận với value != null

Bạn cũng có thể bắt gặp phiên bản ngắn hơn:

values.filter(
  (value): value is string => value != null,
);

Với các giá trị thông thường, cách này trông gần như tương đương với:

value !== null && value !== undefined

Tuy nhiên, browser có một ngoại lệ tương thích lịch sử: document.all.

document.all == null; // true
document.all != null; // false

document.all === null; // false
document.all === undefined; // false

document.all là một object.

Nó không phải null, cũng không phải undefined.

Nhưng loose equality (== / !=) có hành vi đặc biệt với trường hợp này vì lý do tương thích lịch sử.

Vì vậy, nếu contract của bạn thực sự là:

Chỉ loại bỏ nullundefined

thì mình thích dùng strict comparison hơn:

(value): value is string =>
  value !== null && value !== undefined;

Trong code ứng dụng thông thường, bạn gần như sẽ không gặp document.all.

Nhưng edge case này vẫn là một lý do tốt để một nullish guard chính xác không được implement bằng != null.


🔁 Nếu bạn bắt đầu viết check này nhiều lần thì sao?

Phần thú vị bắt đầu khi logic này xuất hiện lặp đi lặp lại:

(value): value is string =>
  value !== null && value !== undefined;

Có thể ở:

  • API adapters
  • selectors
  • UI helpers
  • data transformations
  • utility functions

Lúc này, giá trị của abstraction không chỉ là tiết kiệm vài ký tự.

Quan trọng hơn là đặt tên cho ý nghĩa:

Giá trị này không phải nullish.

Đó là lúc mình thích dùng một named type guard.

Với is-kit:

import { isNotNil } from "is-kit";

const names = values.filter(isNotNil);

TypeScript vẫn narrow chính xác:

// names: string[]

Và khác với Boolean, các giá trị falsy hợp lệ vẫn được giữ lại.

const values: Array<
  string | number | boolean | null | undefined
> = [
  "ready",
  "",
  0,
  false,
  null,
  undefined,
];

const result = values.filter(isNotNil);

// ["ready", "", 0, false]

// result:
// Array<string | number | boolean>

🧩 Một ví dụ gần với ứng dụng thực tế hơn

Các giá trị nullable thường xuất hiện sau map.

Ví dụ:

type User = {
  id: string;
  nickname?: string | null;
};

const users: User[] = [
  { id: "1", nickname: "nyaomaru" },
  { id: "2", nickname: null },
  { id: "3" },
];

Ta chỉ muốn lấy những nickname thực sự tồn tại:

const nicknames = users
  .map((user) => user.nickname)
  .filter(isNotNil);

// string[]

Đây là kiểu tình huống mà reusable type guard khá tự nhiên.

Phần biến đổi array vẫn chỉ là JavaScript bình thường.

Đồng thời TypeScript cũng biết rằng:

Từ đây trở đi, nullundefined đã được loại bỏ.


⚖️ Vậy nên dùng cách nào?

Với mình, tiêu chí khá đơn giản.

Nếu bạn thực sự muốn:

Chỉ giữ lại những giá trị truthy

thì dùng:

filter(Boolean);

Nếu nullish check chỉ xuất hiện một lần:

values.filter(
  (value): value is string =>
    value !== null && value !== undefined,
);

thì inline predicate là hoàn toàn đủ.

Còn nếu cùng một ý nghĩa bắt đầu lặp đi lặp lại, bạn có thể dùng:

values.filter(isNotNil);

một reusable guard.

Không phải mọi condition đều cần abstraction.

Chỉ khi cùng một “ý nghĩa” bắt đầu lặp lại thì việc đặt tên cho nó mới thực sự có giá trị.


🔍 ESLint có thể bắt được vấn đề này không?

Gần đây mình đã phát hành eslint-plugin-is-kit, một type-aware ESLint plugin dành cho TypeScript predicates.

Một trong các rule của nó tìm những trường hợp filter(Boolean) có thể gây hiểu nhầm.

Ví dụ:

declare const values: Array<string | null>;

values.filter(Boolean);

Element type ở đây chứa cả:

  • null
  • và một giá trị falsy nhưng không nullish: ""

Điều đó cho ta một tín hiệu:

Boolean có thể đang loại bỏ nhiều hơn chỉ missing value.

Trong trường hợp đó, rule có thể phát warning.

Nhưng nó không cấm mọi filter(Boolean) một cách máy móc.

Ví dụ:

declare const values: number[];

values.filter(Boolean);

Element type không chứa null hoặc undefined.

Vì vậy, không có bằng chứng rằng ý định của bạn là “loại bỏ nullish”.

Rule này được thiết kế khá bảo thủ.

Phiên bản đầu tiên v0.1.0 có bốn type-aware rules để phát hiện:

  • các filter(Boolean) có thể gây hiểu nhầm
  • các predicate dư thừa của is-kit
  • các inline nullish filter lặp lại có thể dùng isNotNil
  • các inline predicate có thể biểu diễn bằng reusable type guard

Ví dụ:

values.filter(
  (value) => typeof value === "string",
);

có thể được viết thành:

values.filter(isString);

khi runtime behavior và TypeScript narrowing vẫn được giữ nguyên.

Plugin này vẫn còn khá mới, nên nếu bạn gặp false positive hoặc có ý tưởng cho rule mới, mình rất hoan nghênh feedback. 😸


🎯 Điều quan trọng nhất

Điểm chính của bài viết này thực ra không phải là isNotNil.

Mà là:

Falsy value và missing value không phải là một.

filter(Boolean) không phải là code xấu.

Nó chỉ đang biểu đạt một contract khác.

Vì vậy, trước khi viết:

values.filter(Boolean);

hãy thử tự hỏi:

Mình thực sự muốn loại bỏ tất cả giá trị falsy, hay chỉ muốn loại bỏ các giá trị nullish?

Sự khác biệt nhỏ này có thể giúp bạn tránh việc những dữ liệu hoàn toàn hợp lệ như 0, false"" biến mất mà không để ý. 😸

Mình cũng đã viết một guide đầy đủ hơn về nullish filtering trong tài liệu của is-kit, bao gồm isNil, isNotNil và một vài cách tiếp cận khác nhau:

https://is-kit.dev/guides/filter-null-and-undefined

Nếu bạn thích các Type Guard nhỏ, reusable và type-safe trong TypeScript, is-kit cũng là open source:

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

Nếu bài viết hữu ích, một ⭐ trên GitHub luôn rất được hoan nghênh!

Cảm ơn bạ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í