배열에서 null/undefined 거르려고 filter(Boolean)을 쓰는데, 결과 타입이 그대로 (string | undefined)[]로 찍힙니다.
const xs: (string | undefined)[] = ["a", undefined, "b"];
const ys = xs.filter(Boolean); // still (string | undefined)[]
ys.forEach(s => s.toUpperCase()); // ❌ 에러: s가 undefined일 수 있음
filter(Boolean)은 런타임에서는 falsy를 다 거르는데 TypeScript가 왜 이걸 좁혀주지 않는 건가요? tsconfig strict는 true입니다.
댓글 4
타입스크립트가
filter시그니처에 타입 가드 정보를 추론하지 못하기 때문입니다.Boolean은 단지(v: any) => boolean으로 보일 뿐이라, 리턴 타입이 같은 배열 타입으로 유지됩니다.방법 1: 타입 가드 명시 (가장 깔끔)
방법 2: NonNullable 캐스트
방법 3 (5.5+ 공식 해결): Array.prototype.filter의 향상된 추론
TypeScript 5.5부터
filter가 프레디케이트 내부에서 타입 좁힘을 추론하는 능력이 크게 좋아졌습니다. 하지만Boolean같은 일반 함수 참조는 여전히 narrow 대상이 되지 않습니다. 인라인 화살표 함수로 쓰면 추론됩니다.그럼 왜 Boolean을 막지 않나
JavaScript에서
filter(Boolean)은 관용구라 제거할 수 없고, TS는 런타임 동작을 보존하는 원칙이라 타입 가드가 없는 한 좁히지 않습니다. 공통 유틸로isDefined하나 만들어 두면 프로젝트 내내 쓰입니다.저희 팀은
remeda,es-toolkit같은 유틸 라이브러리 쓰는데 거기filter는 처음부터 narrow되게 설계돼서undefined가 자동으로 빠집니다. 커스텀 유틸 늘어나는 게 싫으면 그쪽도 고려해 보세요.isDefined유틸을lib/util/types.ts에 박아두고 썼더니 깔끔해졌습니다. 감사합니다. 그리고 TS 5.5 인라인 화살표 힌트는 처음 알았네요.하나 더 여쭤보면,
filter(x => x)와filter(Boolean)은 런타임 동작 완전히 동일한가요? 성능 차이는 무시 가능 수준인가요?런타임 동작은 거의 동일합니다. 둘 다 falsy(
0,"",null,undefined,NaN,false)를 거르죠. 차이는 함수 호출 오버헤드인데, V8에서는 두 패턴 모두 JIT 최적화 대상이라 실측 차이 0.5% 이내입니다. 성능보다는 "falsy 전체를 거르는 의도인지, undefined만 제외인지"를 코드에서 명확히 하는 쪽이 중요합니다. 숫자 0을 의도적으로 남겨야 한다면Boolean은 버그의 씨앗이 됩니다.