Everyday TypeScript: Logical Operator Narrowing
Welcome to the Logical Operator Narrowing lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We need to write a function that returns the length of its array argument. But if the array is
undefined, it should returnundefined.The first version below uses a regular
ifwith conditional narrowing. In theifside of the conditional, the type ofstringsis known to beundefined. In theelseside, it's known to bestring[].>
function arrayLength(strings: string[] | undefined): number | undefined {if (strings === undefined) {return undefined;} else {return strings.length;}}[arrayLength(['a', 'b']), arrayLength(undefined)];Result:
[2, undefined]
A shorter version of that function is below. It replaces the
ifwith a short-circuiting&&. Don't worry if this is unfamiliar; we'll analyze it in detail after we've seen the example. (The example below does the same thing as the example above.)>
function arrayLength(strings: string[] | undefined): number | undefined {return strings && strings.length;}[arrayLength(['a', 'b']), arrayLength(undefined)];Result:
[2, undefined]
Usually, we think of
&&as an operator that works on boolean values:>
true && false;Result:
false
>
false && false;Result:
false
>
true && true;Result:
true
In JavaScript and TypeScript,
&&works on more than just boolean values. All of the following values act likefalsewhen used in logical operators like&&:false00n(This is a "BigInt", which is part of ECMAScript 2020.)''undefinednullNaN.
All other values act like
true. The usual terms for this are "truthy" and "falsey":0is falsey;57is truthy.&&returns the first falsey value that it finds. If both values are truthy, then&&returns the second value.>
const n: number | undefined = 1;n && 2;Result:
2
>
const n: number | undefined = undefined;n && 2;Result:
undefined
>
const n: number | undefined = 2;n && false;Result:
false
>
const n: number | null = null;n && 2;Result:
null
>
true && 'hello';Result:
'hello'
>
false && true;Result:
false
We can use these rules to create terse conditional expressions like the one in the function we saw above. This one reads as "add 1 to x unless x is null, undefined, or 0":
>
const x = 2;x && x + 1;Result:
3
>
const x = undefined;x && x + 1;Result:
undefined
>
const x = null;x && x + 1;Result:
null
Here's the
arrayLengthfunction again, now that we've seen how the&&trick works:>
function arrayLength(strings: string[] | undefined): number | undefined {return strings && strings.length;}[arrayLength(['a', 'b']), arrayLength(undefined)];Result:
[2, undefined]
This
&&trick works quite well and is common in both JavaScript and TypeScript. There's a similar trick for||, but that requires a new motivating example.We want a function that returns the number it's given. But if the number is
undefined, it should return1.>
function numberOrOne(n: number | undefined): number {if (n === undefined) {return 1;} else {return n;}}[numberOrOne(3), numberOrOne(undefined)];Result:
[3, 1]
Again, we can rewrite it to be shorter by using boolean operators. This time we need
||, which returns the first truthy value it finds. If both values are falsey, then||returns the second value.>
1 || 2;Result:
1
>
const n: number | undefined = undefined;n || 2;Result:
2
>
const n: number | undefined = 2;n || null;Result:
2
>
const b: boolean = false;b || null;Result:
null
Like
&&, we can use this to write short conditionals. This one reads as "return x, or return 2 if x is falsey".>
const x = 5;x || 2;Result:
5
>
const x = undefined;x || 2;Result:
2
>
const x = null;x || 2;Result:
2
Now we'll use
||to write a shorter version ofnumberOrOne:>
function numberOrOne(n: number | undefined): number {return n || 1;}[numberOrOne(3), numberOrOne(undefined)];Result:
[3, 1]
These
&&and||are safer in TypeScript than in JavaScript because the compiler can catch mistakes. For example, accidentally switching&&for||will usually cause a type error, as it does for the incorrect code below. (You can answer withtype errorfor examples like this.)>
function arrayLength(strings: string[] | undefined): number | undefined {return strings || strings.length;}Result:
type error: 'strings' is possibly 'undefined'.
Stepping through what the compiler sees will show us why the error happens:
strings' type isstring[] | undefined.- We're doing
strings || strings.length. - The right side (
strings.length) runs whenstringsis "falsey". stringscan beundefined, which is falsey. Sostringscan beundefinedon the right side of||.- But
undefinedhas nolengthmethod, so we can't dostrings.lengthwhenstringsisundefined. TypeScript says'strings' is possibly 'undefined'.
This is another example of type narrowing, like we saw with
ifin an earlier lesson. Withif, the type was only narrowed inside theif(and inside theelseif there is one). Here, the type is only narrowed on one side of the||. The idea is the same in both cases: TypeScript can tell that we've done something to exclude certain types, so narrows the type accordingly.If it feels difficult to step through this reasoning, don't worry. It's difficult for anyone to think through these tiny logical details without making a mistake. But that's why static type checkers like TypeScript's exist!
The TypeScript compiler is very good at this kind of reasoning. With some practice, most type errors in
&&and||will be obvious to you quickly. In rare cases, there will be some kind of more subtle problem to solve, and you'll have to think through the types step by step, like we did above.That's exactly what we want to happen! If we can't see the problem immediately, that usually means that the type system has found a subtle mistake.
The ternary operator (
... ? ... : ...) also narrows types. Again, we'll start with a clunky example:>
function lengthOrNumber(n: number | number[]): number {if (Array.isArray(n)) {return n.length;} else {return n;}}[lengthOrNumber([1, 2]), lengthOrNumber(5)];Result:
[2, 5]
Now, we write it with
?:.>
function lengthOrNumber(n: number | number[]): number {return Array.isArray(n) ? n.length : n;}[lengthOrNumber([1, 2]), lengthOrNumber(5)];Result:
[2, 5]
The
?:operator doesn't have any major pitfalls; it's just a shorterif. However, there is one big danger with using&&and||in this way. It arises from the fact that0and''are both falsey in JavaScript.That can cause subtle bugs with our
&&and||tricks. Here's one of the examples above, just as we wrote it before. It seems to work!>
function numberOrOne(n: number | undefined): number {return n || 1;}- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
[numberOrOne(3), numberOrOne(undefined)];Result:
[3, 1]
Now here's the bug. Trace through what happens when we pass a
0in. Remember that0acts likefalsein boolean operators and conditionals.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
numberOrOne(0);Result:
1
That's wrong!
0is a number, sonumberOrOne(0)should return0!This is one of the many cases where JavaScript's design leaks through into TypeScript. In JavaScript,
0is falsey. Few people like that, but we can't change it without breaking all existing JavaScript code. TypeScript has to be compatible with JavaScript, so TypeScript can't change it either.Fortunately, tools can help us here. First, we can use linters. For example, there's a typescript-eslint rule that prevents this entire class of mistakes.
We highly recommend using a linter with TypeScript and enabling its strict-boolean-expressions rule. If you do that, then conditional narrowing inside of
&&and||is quite safe. The linter rule also makes regularifand?:conditionals safer for the same reason.The second way that tools can help us is by introducing new operators for terse conditionals, letting us avoid using
&&and||in this way. TypeScript 3.7 did exactly that with the new??operator. We'll see that operator in a later lesson.