Assertion Functions

Playground

There’s a specific set of functions that throw an error if something unexpected happened.They’re called “assertion” functions.As an example, Node.js has a dedicated function for this called assert.

  1. assert(someValue === 42);

In this example if someValue isn’t equal to 42, then assert will throw an AssertionError.

Assertions in JavaScript are often used to guard against improper types being passed in.For example,

  1. function multiply(x, y) {
  2. assert(typeof x === "number");
  3. assert(typeof y === "number");
  4. return x * y;
  5. }

Unfortunately in TypeScript these checks could never be properly encoded.For loosely-typed code this meant TypeScript was checking less, and for slightly conservative code it often forced users to use type assertions.

  1. function yell(str) {
  2. assert(typeof str === "string");
  3. return str.toUppercase();
  4. // Oops! We misspelled 'toUpperCase'.
  5. // Would be great if TypeScript still caught this!
  6. }

The alternative was to instead rewrite the code so that the language could analyze it, but this isn’t convenient.

  1. function yell(str) {
  2. if (typeof str !== "string") {
  3. throw new TypeError("str should have been a string.")
  4. }
  5. // Error caught!
  6. return str.toUppercase();
  7. }

Ultimately the goal of TypeScript is to type existing JavaScript constructs in the least disruptive way.For that reason, TypeScript 3.7 introduces a new concept called “assertion signatures” which model these assertion functions.

The first type of assertion signature models the way that Node’s assert function works.It ensures that whatever condition is being checked must be true for the remainder of the containing scope.

  1. function assert(condition: any, msg?: string): asserts condition {
  2. if (!condition) {
  3. throw new AssertionError(msg)
  4. }
  5. }

asserts condition says that whatever gets passed into the condition parameter must be true if the assert returns (because otherwise it would throw an error).That means that for the rest of the scope, that condition must be truthy.As an example, using this assertion function means we do catch our original yell example.

  1. function yell(str) {
  2. assert(typeof str === "string");
  3. return str.toUppercase();
  4. // ~~~~~~~~~~~
  5. // error: Property 'toUppercase' does not exist on type 'string'.
  6. // Did you mean 'toUpperCase'?
  7. }
  8. function assert(condition: any, msg?: string): asserts condition {
  9. if (!condition) {
  10. throw new AssertionError(msg)
  11. }
  12. }

The other type of assertion signature doesn’t check for a condition, but instead tells TypeScript that a specific variable or property has a different type.

  1. function assertIsString(val: any): asserts val is string {
  2. if (typeof val !== "string") {
  3. throw new AssertionError("Not a string!");
  4. }
  5. }

Here asserts val is string ensures that after any call to assertIsString, any variable passed in will be known to be a string.

  1. function yell(str: any) {
  2. assertIsString(str);
  3. // Now TypeScript knows that 'str' is a 'string'.
  4. return str.toUppercase();
  5. // ~~~~~~~~~~~
  6. // error: Property 'toUppercase' does not exist on type 'string'.
  7. // Did you mean 'toUpperCase'?
  8. }

These assertion signatures are very similar to writing type predicate signatures:

  1. function isString(val: any): val is string {
  2. return typeof val === "string";
  3. }
  4. function yell(str: any) {
  5. if (isString(str)) {
  6. return str.toUppercase();
  7. }
  8. throw "Oops!";
  9. }

And just like type predicate signatures, these assertion signatures are incredibly expressive.We can express some fairly sophisticated ideas with these.

  1. function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
  2. if (val === undefined || val === null) {
  3. throw new AssertionError(
  4. `Expected 'val' to be defined, but received ${val}`
  5. );
  6. }
  7. }

To read up more about assertion signatures, check out the original pull request.