Wanna see something cool? Check out Angular Spotify 🎧

TypeScript is Operator for Type Narrowing

Problem

TypeScript’s Control Flow Analysis is generally effective at narrowing types. For instance, starting from TypeScript 4.4, type guards can influence control flow more effectively. Consider the following example:

// In TS 4.3 and below
function foo(arg: unknown) {
  const argIsString = typeof arg === "string";
  if (argIsString) {
    console.log(arg.toUpperCase());
    //              ~~~~~~~~~~~
    // Error! Property 'toUpperCase' does not exist on type 'unknown'.
  }
}

In earlier versions of TypeScript, even though argIsString is assigned the result of a type guard (typeof arg === "string"), TypeScript would lose that information, resulting in an error. In TypeScript 4.4 and above, the control flow is improved, and this issue is resolved:

// In TS 4.4 and above
function foo(arg: unknown) {
  const argIsString = typeof arg === "string";
  if (argIsString) {
    console.log(arg.toUpperCase()); // No error, TypeScript knows arg is a string now.
  }
}

However, there are still situations where TypeScript’s type inference falls short. For example, when filtering an array that contains undefined, null, or other falsy values, TypeScript can’t determine whether the filtered array is completely truthy.

const fruits = ['apple', undefined, 'banana', undefined, 'cherry']; // (string | undefined)[]

const filteredFruits = fruits.filter(item => !!item); // (string | undefined)[]

Even though the undefined values have been filtered out, TypeScript still thinks the result is of type (string | undefined)[], instead of string[]. This can lead to type issues down the line, requiring manual type assertions or dealing with incorrect type assumptions.

TypeScript is Operator for Type Narrowing

Solution

To resolve this, you can use a custom type guard with TypeScript’s is operator, which explicitly informs TypeScript of the correct type. Here’s how to create an isTruthyPredicate function:

export function isTruthyPredicate<SomeType>(
  item: SomeType | boolean | null | undefined,
): item is SomeType {
  return !!item;
}

This predicate ensures that TypeScript understands when an item is truthy. By using this function in a filter operation, you can ensure TypeScript infers the correct type:

const filteredFruitsOnlyTruthy = fruits.filter(isTruthyPredicate); // string[]

Now, filteredFruitsOnlyTruthy is correctly inferred as string[], eliminating the need for type assertions and making the code safer.

Why use the is Operator for Type Narrowing?

When searching, I found this answer is sensible too.

https://stackoverflow.com/q/51124837/3375906

In TypeScript, a boolean is just a data type that can be either true or false, whereas the is operator is specifically used for type-testing. It tells TypeScript that a particular condition not only checks a value but also narrows down its type. Let’s take a look at an example to understand the difference:

type Species = 'cat' | 'dog';

interface Pet {
    species: Species;
}

class Cat implements Pet {
    public species: Species = 'cat';
    public meow(): void {
        console.log('Meow');
    }
}

function petIsCat(pet: Pet): pet is Cat {
    return pet.species === 'cat';
}

function petIsCatBoolean(pet: Pet): boolean {
    return pet.species === 'cat';
}

const p: Pet = new Cat();

p.meow(); // ERROR: Property 'meow' does not exist on type 'Pet'.

if (petIsCat(p)) {
    p.meow(); // Now the compiler knows for sure that 'p' is of type Cat, and it has a 'meow' method
}

if (petIsCatBoolean(p)) {
    p.meow(); // ERROR: Property 'meow' does not exist on type 'Pet'.
}

In this example:

  • petIsCat is a custom type guard using the is operator. It tells TypeScript that if the condition is true, the pet is of type Cat, and thus, the meow method is available.
  • petIsCatBoolean only returns a boolean, and while it performs the same check, it doesn’t narrow the type. As a result, TypeScript doesn’t know that p is a Cat and throws an error when trying to access meow.

Playground Example

You can see the full example in the TypeScript playground here.

Conclusion

While TypeScript’s Control Flow Analysis has improved over the years, as seen in features introduced in TypeScript 4.4, there are still cases, like filtering out falsy values, where the type inference doesn’t work as expected. In such situations, custom type predicates using the is operator can be a clean and effective solution to ensure TypeScript narrows types properly, improving both safety and developer experience.

Published 22 Sep 2024

    Read more

     — nvm keeps "forgetting" node version in new VSCode terminal sessions
     — An error occurred while installing pg (1.5.6), and Bundler cannot continue (when running rails new)
     — Copy Code with Syntax Highlighting from VSCode to PowerPoint
     — Notion: Rounding to 2 Decimal Places
     — Adding "loading=lazy" to GIF files on my 7-year-old Gatsby blog (with ChatGPT assistance)