Nazar Hussain
Nazar Hussain

Reputation: 5162

Typescript truthiness narrowing with conditional operators

Considering the type truthiness narrowing if we have the following types and variables.

type T = string | null | undefined; 
type T2 = string;

const getResult = (): T => '' as T;

const v1:T = getResult();
const v2:T = getResult();

The following script must not throw any error. As:

  1. The if exhausts the truthiness for both variables.
  2. After the if any of the v1 and v2 must exists.
if(!v1 && !v2) {
    throw new Error('Value Error');
}

const r: T2 = v1 ?? v2;

But the above script don't compile with following error on the assignment.

Type 'undefined' is not assignable to type 'string'

In contrast the following script works fine, thought the type narrowing must be same in both cases.

if(v1 && v2) {
    const r: T2 = v1 ?? v2;
}

Is it a some Typescript bug or some other issue?

Upvotes: 0

Views: 492

Answers (1)

VLAZ
VLAZ

Reputation: 28986

The opposite of the boolean expression v1 && v2 is not !v1 && !v2.

According to De Morgan's laws

not (A and B) = (not A) or (not B)

Thus the opposite would be

!(v1 && v2) = !v1 || !v2

Conversely

not (A or B) = (not A) and (not B)

Thus the first boolean expression is actually the same as a negated OR

!(v1 || v2) = !v1 && !v2

That means that the opposite (when no error would be thrown) would be a normal OR.

This is easy to prove with a boolean table:

v1 v2 v1 && v2 !v1 && !v2 !(v1 || v2) v1 || v2
TRUE TRUE TRUE FALSE FALSE TRUE
TRUE FALSE FALSE FALSE FALSE TRUE
FALSE TRUE FALSE FALSE FALSE TRUE
FALSE FALSE FALSE TRUE TRUE FALSE

Therefore there is no bug in TypeScript. It works respecting logic.

What is missing is that the compiler cannot represent the middle two rows of cases - when the values are mixed. That requires the compiler to know that the only possible pairings of types are v1: T; v2: T2 and v1: T2; v2: T - yet such "dependent types" do not exist in TypeScript.


For example v1 = null and v2 = "foo" the if statement will not trigger. That means that the compiler knows that both are not T2 at the same time. Since it cannot express "one is T other is T2". It considers each variable individually - since it cannot guarantee that v1 is T2, nor can it guarantee that v2 is T2 (remember - no dependencies) it can only represent each variable as T.

This accounts for the failure to narrow after the if.


One option to make the compiler understand what you mean is

if(!v1) {
    throw new Error('Value Error');
}

if(!v2) {
    throw new Error('Value Error');
}

const r: T2 = v1 ?? v2;

Playground Link

This is logically equivalent to !v1 && !v2 but works with the narrowing, since each variable is examined separately, thus no need for dependency between them at compiler level. After the first if passes, then v1 is T2 and nothing else. After the second if passes, then v2 is T2 and nothing else. The only way to get to const r: T2 = v1 ?? v2 is if neither is falsy. Although, this introduces an oddity where r is guaranteed to only ever be v1.

Another option might be even simpler:

const r: T = v1 ?? v2;

if(!r) {
    throw new Error('Value Error');
}

Playground Link

This is again logically equivalent, since the result of the logical operation is used. After the if the variable r is guaranteed to be T2 because it is not falsy.

Upvotes: 2

Related Questions