Rowanto
Rowanto

Reputation: 2959

Why doesn't type narrowing with double negation work in typescript?

If there is a double negative condition such as below, typescript is not able to resolve the type. In here, it will have a compile error on the variable c.

const testFunction = (a: number | null, b: number | null) => {
    if (!a && !b) {
        return;
    }
    const c: number = a || b;
}

I know that we can actually rewrite this with if else and throw exception. I am just wondering why makes this difficult to implement in typescript, and is there no more elegant way to write this in a way that typescript will understand?

Upvotes: 6

Views: 209

Answers (1)

jcalz
jcalz

Reputation: 328342

TypeScript's control flow analysis only works on individual variables or properties of variables, and it works on them independently. So with a check like if (!a && !b) { /* true */ } else { /* false */ } the compiler will understand that a must be falsy and that b must be falsy inside the true block, but there is no corresponding independent narrowing that can be done to a and b inside the false block. So a and b both stay their full number | null types there.

In order for your code as written to be verified as safe, the compiler would need to keep track of the state of not just each variable, but each expression (like a || b) or each set of variables (like [a, b]). This isn't a big deal for two variables, but for the general case this is essentially impossible. The number of sets of variables grows exponentially with the number of variables, and there wouldn't be enough time or memory for the compiler to accurately keep track of the full state in even a modestly sized function.

There was a feature request at microsoft/TypeScript#30869 asking for the ability to track such things, but it was closed as "too complex".


That leaves us with refactoring. If you care about !a && !b or its logical sibling a || b, you should copy the relevant value to its own variable c first, and then check that for truthiness. This reduces the task of tracking some combination of a and b to the task of tracking just c, which the compiler is much better at doing:

const testFunction = (a: number | null, b: number | null) => {
    const c = a || b; // c is known as number | null here
    if (!c) return;
    c.toFixed(2) // okay, c is known to be number here
}

Playground link to code

Upvotes: 3

Related Questions