TypeScript: Boolean() cannot be used to perform a null check

TypeScript Version: 2.4.0

Apologies for today’s issue raising binge.

Code

// Compiles
const nullCheckOne = (value?: number) => {
    if (!!value) {
        return value.toFixed(0);
    }

    return '';
}

const nullCheckTwo = (value?: number) => {
    if (Boolean(value)) {
        // Object is possibly 'undefined'
        return value.toFixed(0);
    }

    return '';
}

Expected behavior: Both examples compile.

Actual behavior: The latter example fails w/ Object is possibly 'undefined'.

Explanation To my knowledge !!value and Boolean(value) are equivalent. I’m wondering what is the reason behind not supporting the second case. One reason I can think of would be an imported, non-typescript module, globally overriding it to something like: Boolean = (value) => !value.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 71
  • Comments: 22 (7 by maintainers)

Commits related to this issue

Most upvoted comments

One common pattern using the boolean constructor is .filter(Boolean) to remove falsy values from an array (including null and undefined).

I think the “Awaiting more feedback” label can be removed, as tons of people seem to have this issue.

For anyone else running into the specific case of Array.filter(Boolean), adding an overload for Array.prototype.filter() works great. I’ve tested this on 4.1.2 and https://github.com/microsoft/TypeScript/issues/31551#issuecomment-495201128 doesn’t seem to be present anymore. Patching Boolean itself should work too, but I haven’t tried that.

type Falsy = false | 0 | "" | null | undefined;

interface Array<T> {
  filter<S extends T>(predicate: BooleanConstructor, thisArg?: any): Exclude<S, Falsy>[];
}

Another pattern is using Boolean() along with && - a common pattern for example in embedded JS expressions in JSX with React:

function CartMessage(props: { cartItems: number }) {
  return <div>Cart contains ${props.cartItems} items</div>;
}

export default function App() {
  const cartItems = 0 as number | null;

  return (
    <div className="App">
      <h3>Using number on LHS of &&</h3>
      <h4>(see 0 below)</h4>
      <div>{cartItems && <CartMessage cartItems={cartItems} />}</div>

      <h3>Using Boolean() on LHS of &&</h3>
      <h4>(no 0 below)</h4>
      <div>{Boolean(cartItems) && <CartMessage cartItems={cartItems} />}</div>
    </div>
  );
}

https://codesandbox.io/s/falling-https-zvg2xv?file=/src/App.tsx

In the first example, the 0 is rendered on the screen because it is falsey:

Screen Shot 2022-10-03 at 18 37 04

To avoid this, the Boolean() constructor is used to convert the number on the LHS of the && to a boolean, but this causes a problem with the types:

Type 'number | null' is not assignable to type 'number'.
  Type 'null' is not assignable to type 'number'.ts(2322)

If the Boolean() constructor behaved the same way that !! (double negation) did, this wouldn’t be an issue.

Also affects strings:

let name = '' as string | null;

// Renders empty string, usually not a problem but can cause crashes with React Native
// https://github.com/facebook/react-native/issues/20764
{name && <Profile name={name} />}

// 💥 Type 'string | null' is not assignable to type 'string'.
{Boolean(name) && <Profile name={name} />}

Alternatives

There are alternatives to doing this:

// Double negation:
{!!cartItems && <CartMessage cartItems={cartItems} />}

// Greater than:
{cartItems > 0 && <CartMessage cartItems={cartItems} />}

But sometimes the Boolean() option may be desirable because of its expressiveness.

@Kovensky - that is a good point. filter as it currently stands however, can’t be used to implement the type guards either. Both of the examples below infer to (number | undefined)[]. Would also be really cool to address it.

const nonFalsy1 = [1, 2, undefined].filter(v => typeof v !== 'undefined');
const nonFalsy2 = [1, 2, undefined].filter(Boolean);

This was reverted due to it causing a breaking change in 3.5. The simple repro looks like this:


declare const Bullean: {
    new(value?: any): Boolean;
    <T>(value?: T): value is Exclude<T, false | null | undefined | '' | 0>;
};

declare const arr: any[];
// Was any[], now (3.5) unknown[]
const arr2 = arr.filter(Bullean);

There might be something wrong in generic inference, but it’s not yet clear. We’ll need this scenario to work before adding it back in.

const workaround = arr.filter((elem): elem is number => elem !== undefined);

Another workaround (playground link):

const values: Array<number | undefined> = [1, 2, undefined, 4, undefined, 6]

function nonNullish<V>(v:V): v is NonNullable<V> {
    return v !== undefined && v !== null
}

values.filter(nonNullish).map(v => v.toFixed(5))

For anybody using @Etheryte’s great tip from https://github.com/microsoft/TypeScript/issues/16655#issuecomment-797044678 you may also want to extend the ReadonlyArray interface with the same fix:

type Falsy = false | 0 | '' | null | undefined;
interface Array<T> {
  filter<S extends T>(predicate: BooleanConstructor, thisArg?: any): Exclude<S, Falsy>[];
}
interface ReadonlyArray<T> {
  filter<S extends T>(predicate: BooleanConstructor, thisArg?: any): Exclude<S, Falsy>[];
}

I’m wondering what is the reason behind not supporting the second case

It’s not supported because no one has done the work to support it yet.

One reason I can think of would be an imported, non-typescript module, globally overriding it to something

In general we don’t account for this. Literally anything in the global scope could be mutated to be something different and it’d be insane to say Well you can’t use substr because maybe someone messed with it.

I think we could replace the signature with

declare function Boolean<T>(x: T | false | 0 | "" | null | undefined): x is T;

which is almost correct (it fails to do the right then in the else case when x is 0 coming from a non-literal numeric type).

I’ve literally never seen someone use Boolean like this, though. Is there any difference between if (value) and if (Boolean(value)) ?

Nice one @Etheryte, thank you!

Here’s a solution that doesn’t require importing/defining a function, nor overloading a prototype. Verbose as hell, but 🤷

const arr: (Thing | undefined)[] = list.map(...) // may return undefined, or may return a Thing
arr.filter((thing): thing is Thing => !!thing)

Here’s a solution that doesn’t require importing/defining a function, nor overloading a prototype. Verbose as hell, but 🤷

const arr: (Thing | undefined)[] = list.map(...) // may return undefined, or may return a Thing
arr.filter((thing): thing is Thing => !!thing)

cool! btw, what’s the difference between your example and this one?:

arr.filter(Boolean) as Thing[]
const arr = [1, 2, undefined];
const filtered: Array<number> = arr.filter(elem => elem != undefined);
const filtered2: Array<number> = arr.filter(Boolean);

// Type '(number | undefined)[]' is not assignable to type 'number[]'.
//   Type 'number | undefined' is not assignable to type 'number'.
//     Type 'undefined' is not assignable to type 'number'.

// Type '(number | undefined)[]' is not assignable to type 'number[]'.

Both these scenarios right now still break. I found a temporary workaround with

const workaround = arr.filter((elem): elem is number => elem !== undefined);

However it would be nice not to need the type guard. Is there any hope for that 😃?