Search code examples
javascripttypescripttypesassertiontypechecking

What is the difference between "asserts value is type" and "value is type" in TypeScript?


TypeScript has is operator which helps to create a test function for type checking. Recently I saw two different implementations of this operator, one of which uses asserts keyword.

I didn't find information about the difference of the two ways of use in the docs. I played with it a little and if I'm correct, asserts doesn't let you return anything from the function, but other than this I didn't find any differences.

Here is the code I tested with:

// Asserts and tests the value without returninng anything
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw Error("value is not a string");
}

// Tests the value and returns something so it can be used for typecheck
// more explicitly
function testIsString(value: unknown): value is string {
  return typeof value === "string";
}

const string = "hello";
const number = 123;

assertIsString(string); // does nothing
assertIsString(number); // throws Error
testIsString(string); // returns true
testIsString(number); // returns false

Question: Are there other differences between the two use cases?


Solution

  • Summary: The main difference is that one throws while the other has to be used in a conditional.


    The functions which potentially throw an exception and return void are called assertion functions.

    These make an assertion (you might think of it as creating a contract with the compiler), that if the function doesn't throw an exception, the predicate in the return value will be true. From that point onward (within the current scope), the type information in the predicate will be in effect.


    The functions which return boolean values are called type predicates.

    Instead of potentially throwing an exception (and causing your program to crash unless it's caught — see try...catch), these simply return a boolean value. If the boolean is true, then for the remainder of the scope where the predicate was invoked (e.g. a block of code), the predicate will be in effect.


    The documentation links have several examples for each case (and additional information). Here's a demo:

    TS Playground

    // predicate
    function exists<T>(maybe: T): maybe is NonNullable<T> {
      return maybe != null;
    }
    
    // assertion
    function assertExists<T>(maybe: T): asserts maybe is NonNullable<T> {
      if (maybe == null) throw new Error(`${maybe} doesn't exist`);
    }
    
    function example1() {
      console.log("example1 begin");
      let maybe: string | undefined;
    
      if (exists(maybe)) {
        maybe; // string
      } else {
        maybe; // undefined
      }
    
      console.log("example1 end");
    }
    
    function example2() {
      console.log("example2 begin");
      let maybe: string | undefined;
    
      assertExists(maybe);
    
      maybe; // string
    
      console.log("example2 end");
    }
    
    example1(); // 'example1 begin' then 'example1 end'
    example2(); // only 'example2 begin', then exception is thrown: `undefined doesn't exist`