Reputation: 5162
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:
if
exhausts the truthiness for both variables.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
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;
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');
}
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