Predicate inference fails for empty object and nullish values #58967
Description
🔎 Search Terms
"infer predicate object types", "infer predicate empty object", "infer predicate empty object null", "{} | null"
🕗 Version & Regression Information
- This is a type narrowing issue
- This is the behavior in every version I tried
⏯ Playground Link
💻 Code
const emptyNull = [{}, null].filter(s => !!s); // expected {}[]
// ^? ({} | null)[]
const emptyUndefined = [{}, undefined].filter(s => !!s); // expected {}[]
// ^? ({} | undefined)
const emptyTruthy = [{}].filter(s => !!s); // expected {}[]
// ^? {}[]
const emptyFalsy = [{}].filter(s => !s); // expected never[]
// ^? {}[]
const nullish = [null, undefined].filter(s => !!s); // expected never[]
// ^? (null | undefined)[]
const explicitNullish = [{}, null].filter(s => s !== null); // ok
// ^? { foo: {}; }[]
const nonEmptyNull = [{ foo: {} }, null].filter(s => !!s); // ok
// ^? { foo: {}; }[]
const emptyNumber = [{}, 0].filter(s => !!s); // ok
// ^? {}[]🙁 Actual behavior
Predicate inference fails for the cases where {} is declared along with a nullish value, using a predicate to test for truthiness or falsiness (!!x / !x). It infers a type that includes the nullishness.
It also fails to infer the predicate for cases where array only contains nullish values.
It works if the object has at least one property, or using explicit bullishness check.
🙂 Expected behavior
The return type should reflect the truthiness of {}, as it is neither a nullish or falsy value and should pass the truthiness test.
Additional information about the issue
The section "This is a gentle nudge away from truthiness footguns" in #57465 explains that object types should have no footguns:
If you're working with object types, on the other hand, there is no footgun
and a truthiness test will infer a predicate:const datesTruthy = [new Date(), null, new Date(), null].filter(d => !!d); // ^? const datesTruthy: Date[]
That's why I expected the same for {} and the opposite with nullish-only values.
Activity
fatcerberus commentedon Jun 21, 2024
Primitives are assignable to
{}, unlike (most) other object types.jcalz commentedon Jun 22, 2024
And because
""and0both match{}, you can't say that!ximplies thatxisnull. And without direct support for #15048 you can't use a type predicate for this at all. You'd like to say that(x: {} | null) => !!xreturns(x is {}) else falseor something, and redefinefilter()so it only cares about the true side of the type guard. But we don't have that so 🤷♂️typescript-bot commentedon Jun 27, 2024
This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.