왜 `filter(Boolean)`은 TypeScript에서 Nullish Filter가 아닐까? 🔧

nyaomaru·2026년 9월 18일

안녕하세요! 👋

저는 @nyaomaru이고, Frontend Engineer로 일하면서 작은 TypeScript OSS 프로젝트도 만들고 있습니다. 😸

오늘은 아주 짧고 익숙한 JavaScript 코드 한 줄에 대해 이야기해 보려고 합니다.

values.filter(Boolean);

아마 한 번쯤은 보셨을 겁니다.

  • 짧고
  • 편리하고
  • 때로는 정말 원하는 동작과 정확히 맞습니다

하지만 실제 의도가 다음이라면 어떨까요?

null과 undefined만 제거하고 싶다

이 경우 filter(Boolean)은 생각보다 더 많은 값을 제거합니다.

왜 그런지 살펴보겠습니다. 👀


🕳️ filter(Boolean)의 작은 함정

다음과 같은 배열이 있다고 해봅시다.

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

이제 “없는 값”만 제거하고 싶다고 해보겠습니다.

const result = values.filter(Boolean);

runtime에서 남는 값은 무엇일까요?

["ready"];

잠깐만요.

우리가 제거하고 싶었던 것은:

null;
undefined;

뿐이었습니다.

하지만 이것들도 함께 사라졌습니다.

"";
0;
false;

왜일까요?

Boolean은 전달받은 값을 boolean으로 변환하고, 결과가 true인 값만 남기기 때문입니다.

즉, 다음을 검사하는 것이 아닙니다.

이 값이 null 또는 undefined인가?


🤔 Falsy와 Nullish는 다르다

JavaScript에서 다음 값들은 모두 falsy입니다.

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

하지만 아래 두 요구사항은 완전히 다릅니다.

모든 falsy 값을 제거한다

그리고:

null과 undefined만 제거한다

실제 애플리케이션에서는 0, false, ""가 모두 정상적인 데이터일 수 있습니다.

예를 들어:

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

다음 값들은 모두 유효할 수 있습니다.

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

Falsy는 missing을 의미하지 않습니다.


🧠 TypeScript에서도 차이가 있다

filter(Boolean)은 runtime에서 falsy 값을 제거하지만, Boolean 자체는 type guard가 아닙니다.

그래서 TypeScript는 일반적으로 filter(Boolean)을 사용했다고 해서 null과 undefined가 제거되었다고 판단하지 못합니다.

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>

즉, runtime 규칙이 정말로:

truthy 값만 남긴다

라면 filter(Boolean)은 전혀 문제가 없습니다.

하지만 다음 의도를 표현하는 코드는 아닙니다.

nullish 값만 제거한다

이 차이는 코드를 읽는 사람에게도 중요하고, TypeScript의 type system에게도 중요합니다.


✅ 실제로 원하는 규칙을 그대로 쓰자

원하는 규칙이:

null과 undefined를 제외한 모든 값을 남긴다

라면 그대로 작성하면 됩니다.

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

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

이제:

// names: string[]

이건 아주 평범한 TypeScript입니다.

이 check가 한 번만 필요하다면 굳이 library를 사용할 필요도 없습니다.


⚠️ Browser Edge Case: value != null은 주의

더 짧은 형태도 자주 볼 수 있습니다.

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

일반적인 값만 생각하면 다음과 거의 같은 것처럼 보입니다.

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

하지만 browser에는 역사적인 호환성 예외가 하나 있습니다.

바로 document.all입니다.

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

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

document.all은 object입니다.

null도 아니고 undefined도 아닙니다.

하지만 loose equality (== / !=)는 과거 호환성 때문에 이 값에 대해 특별한 동작을 합니다.

그래서 contract가 정말로:

오직 null과 undefined만 제거한다

라면 저는 strict comparison을 더 선호합니다.

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

일반적인 애플리케이션 코드에서 document.all을 만날 가능성은 거의 없습니다.

그래도 이 edge case는 정확한 nullish guard를 != null로 구현하지 않는 좋은 이유가 됩니다.


🔁 이 check가 반복되기 시작한다면?

흥미로운 지점은 아래 로직이 여러 곳에서 반복되기 시작할 때입니다.

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

예를 들면:

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

이 시점에서 abstraction의 가치는 단순히 몇 글자를 줄이는 데 있지 않습니다.

더 중요한 것은 이 의미에 이름을 붙이는 것입니다.

이 값은 nullish가 아니다.

이럴 때 저는 named type guard를 사용하는 편입니다.

is-kit에서는 이렇게 쓸 수 있습니다.

import { isNotNil } from "is-kit";

const names = values.filter(isNotNil);

TypeScript narrowing도 그대로 유지됩니다.

// names: string[]

그리고 Boolean과 달리 유효한 falsy 값은 사라지지 않습니다.

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>

🧩 실제 애플리케이션에 가까운 예시

nullable value는 map 이후에 자주 등장합니다.

예를 들어:

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

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

존재하는 nickname만 얻고 싶다면:

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

// string[]

이런 경우 reusable type guard가 꽤 자연스럽습니다.

배열 변환 자체는 여전히 평범한 JavaScript입니다.

동시에 TypeScript도 다음 사실을 알 수 있습니다.

여기부터는 null과 undefined가 없다.


⚖️ 그럼 어떤 방식을 써야 할까?

제 기준은 꽤 단순합니다.

정말 원하는 것이:

truthy 값만 남긴다

라면:

filter(Boolean);

을 사용하면 됩니다.

nullish check가 한 번만 등장한다면:

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

이런 inline predicate면 충분합니다.

반대로 같은 의미가 여러 곳에서 반복된다면:

values.filter(isNotNil);

같은 reusable guard를 고려할 수 있습니다.

모든 condition을 abstraction으로 만들 필요는 없습니다.

같은 “의미”가 반복될 때 이름을 붙이는 것이 더 중요합니다.


🔍 ESLint로 이런 문제를 찾을 수 있을까?

최근 eslint-plugin-is-kit을 공개했습니다.

TypeScript predicate를 위한 type-aware ESLint plugin입니다.

그중 한 rule은 의미가 애매한 filter(Boolean) 호출을 찾아냅니다.

예를 들어:

declare const values: Array<string | null>;

values.filter(Boolean);

여기 element type에는:

  • null
  • 그리고 nullish가 아닌 falsy 값 ""

이 함께 존재합니다.

즉 다음 가능성이 있습니다.

Boolean이 missing value만 제거하는 것이 아니라 유효한 데이터까지 제거할 수 있다.

이 경우 rule이 warning을 낼 수 있습니다.

하지만 모든 filter(Boolean)을 무조건 금지하지는 않습니다.

예를 들어:

declare const values: number[];

values.filter(Boolean);

element type에는 null이나 undefined가 없습니다.

따라서 “nullish 값을 제거하려고 했다”는 근거가 없습니다.

이 rule은 의도적으로 보수적으로 동작합니다.

초기 버전 v0.1.0에는 다음 네 가지 type-aware rule이 있습니다.

  • 의미가 애매한 filter(Boolean)
  • 불필요한 is-kit predicate
  • isNotNil로 표현할 수 있는 반복적인 inline nullish filter
  • reusable type guard로 표현할 수 있는 inline predicate

예를 들어:

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

runtime behavior와 TypeScript narrowing이 동일하게 유지된다면:

values.filter(isString);

처럼 표현할 수 있습니다.

이 plugin은 아직 초기 단계이므로 false positive나 새로운 rule 아이디어가 있다면 피드백을 정말 환영합니다. 😸


🎯 가장 중요한 포인트

사실 이 글에서 가장 중요한 것은 isNotNil 자체가 아닙니다.

핵심은 이것입니다.

Falsy value와 missing value는 같은 것이 아니다.

filter(Boolean)은 나쁜 코드가 아닙니다.

단지 다른 contract를 표현할 뿐입니다.

그래서 다음 코드를 쓰기 전에:

values.filter(Boolean);

한 번 이렇게 생각해볼 수 있습니다.

정말 모든 falsy 값을 제거하고 싶은가?
아니면 nullish 값만 제거하고 싶은가?

이 작은 차이만 의식해도 0, false, ""처럼 완전히 유효한 데이터가 조용히 사라지는 문제를 피할 수 있습니다. 😸

is-kit 문서에도 nullish filtering에 대한 더 자세한 guide를 작성해 두었습니다.

isNil, isNotNil, 그리고 여러 접근 방식을 함께 다룹니다.

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

작고 reusable한 TypeScript Type Guard에 관심이 있다면 is-kit도 open source로 공개되어 있습니다.

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

도움이 되었다면 GitHub에서 ⭐도 정말 환영합니다!

읽어주셔서 감사합니다! 🙌

profile
Funny Frontend Engineer 😸

0개의 댓글