Execute Program

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 return undefined.

  • The first version below uses a regular if with conditional narrowing. In the if side of the conditional, the type of strings is known to be undefined. In the else side, it's known to be string[].

  • >
    function arrayLength(strings: string[] | undefined): number | undefined {
    if (strings === undefined) {
    return undefined;
    } else {
    return strings.length;
    }
    }
    [arrayLength(['a', 'b']), arrayLength(undefined)];
    Result:
    [2, undefined]Pass Icon
  • A shorter version of that function is below. It replaces the if with 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]Pass Icon
  • Usually, we think of && as an operator that works on boolean values:

  • >
    true && false;
    Result:
    falsePass Icon
  • >
    false && false;
    Result:
    falsePass Icon
  • >
    true && true;
    Result:
    truePass Icon
  • In JavaScript and TypeScript, && works on more than just boolean values. All of the following values act like false when used in logical operators like &&:

    • false
    • 0
    • 0n (This is a "BigInt", which is part of ECMAScript 2020.)
    • ''
    • undefined
    • null
    • NaN.
  • All other values act like true. The usual terms for this are "truthy" and "falsey": 0 is falsey; 57 is 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:
    2Pass Icon
  • >
    const n: number | undefined = undefined;
    n && 2;
    Result:
    undefinedPass Icon
  • >
    const n: number | undefined = 2;
    n && false;
    Result:
    falsePass Icon
  • >
    const n: number | null = null;
    n && 2;
    Result:
    nullPass Icon
  • >
    true && 'hello';
    Result:
    'hello'Pass Icon
  • >
    false && true;
    Result:
    falsePass Icon
  • 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:
    3Pass Icon
  • >
    const x = undefined;
    x && x + 1;
    Result:
    undefinedPass Icon
  • >
    const x = null;
    x && x + 1;
    Result:
    nullPass Icon
  • Here's the arrayLength function 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]Pass Icon
  • 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 return 1.

  • >
    function numberOrOne(n: number | undefined): number {
    if (n === undefined) {
    return 1;
    } else {
    return n;
    }
    }
    [numberOrOne(3), numberOrOne(undefined)];
    Result:
    [3, 1]Pass Icon
  • 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:
    1Pass Icon
  • >
    const n: number | undefined = undefined;
    n || 2;
    Result:
    2Pass Icon
  • >
    const n: number | undefined = 2;
    n || null;
    Result:
    2Pass Icon
  • >
    const b: boolean = false;
    b || null;
    Result:
    nullPass Icon
  • 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:
    5Pass Icon
  • >
    const x = undefined;
    x || 2;
    Result:
    2Pass Icon
  • >
    const x = null;
    x || 2;
    Result:
    2Pass Icon
  • Now we'll use || to write a shorter version of numberOrOne:

  • >
    function numberOrOne(n: number | undefined): number {
    return n || 1;
    }
    [numberOrOne(3), numberOrOne(undefined)];
    Result:
    [3, 1]Pass Icon
  • 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 with type error for examples like this.)

  • >
    function arrayLength(strings: string[] | undefined): number | undefined {
    return strings || strings.length;
    }
    Result:
    type error: 'strings' is possibly 'undefined'.Pass Icon
  • Stepping through what the compiler sees will show us why the error happens:

    • strings' type is string[] | undefined.
    • We're doing strings || strings.length.
    • The right side (strings.length) runs when strings is "falsey".
    • strings can be undefined, which is falsey. So strings can be undefined on the right side of ||.
    • But undefined has no length method, so we can't do strings.length when strings is undefined. TypeScript says 'strings' is possibly 'undefined'.
  • This is another example of type narrowing, like we saw with if in an earlier lesson. With if, the type was only narrowed inside the if (and inside the else if 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]Pass Icon
  • 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]Pass Icon
  • The ?: operator doesn't have any major pitfalls; it's just a shorter if. However, there is one big danger with using && and || in this way. It arises from the fact that 0 and '' 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]Pass Icon
  • Now here's the bug. Trace through what happens when we pass a 0 in. Remember that 0 acts like false in boolean operators and conditionals.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    numberOrOne(0);
    Result:
    1Pass Icon
  • That's wrong! 0 is a number, so numberOrOne(0) should return 0!

  • This is one of the many cases where JavaScript's design leaks through into TypeScript. In JavaScript, 0 is 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 regular if and ?: 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.