Execute Program

Advanced TypeScript: Assertion Functions

Welcome to the Assertion Functions lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • An assertion function throws an error unless some condition is true. We'll start by defining the basic form below.

  • (In these examples, you can type error if an example will cause a thrown error at runtime. When this assert function doesn't throw an error, it will return undefined.)

  • >
    function assert(condition: boolean) {
    if (!condition) {
    throw new Error('Assertion failed');
    }
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    assert(1 + 1 === 2);
    Result:
    undefinedPass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    assert(1 + 1 === 3);
    Result:
    Error: Assertion failedPass Icon
  • This function guarantees that the condition is true. We can tell that by looking at it. But the TypeScript compiler doesn't know that.

  • Let's assume that var1 has the type number | string. What happens if we call assert(typeof var1 === 'number')? The code after that point will only run if assert didn't throw an error. That means that var1 must've actually been a number. But TypeScript doesn't know that; it still thinks that it's a number | string.

  • (When a code example has a type error, you can answer with type error.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const var1: number | string = ((): number | string => 1)();
    assert(typeof var1 === 'number');
    const var2: number = var1;
    Result:
    type error: Type 'string | number' is not assignable to type 'number'.
      Type 'string' is not assignable to type 'number'.Pass Icon
  • Remember that we can narrow types by using if (typeof x === ...). In the next example, we narrow a variable's type from number | string to just number. The TypeScript compiler allows this because it knows that var1 has the type number inside the if.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const var1: number | string = ((): number | string => 1)();
    let var2: number;
    if (typeof var1 === 'number') {
    var2 = var1;
    } else {
    var2 = 0;
    }
    var2;
    Result:
    1Pass Icon
  • TypeScript 3.7 allows assertion functions to narrow types, kind of like how type guards narrow them. It requires one small tweak to the assert function. We tell the TypeScript compiler what we already know: if the assert function returns at all, then the condition that we passed to it must be true.

  • In this example, note the new asserts condition syntax. It's referring to condition, the argument to the function.

  • >
    function assert(condition: boolean): asserts condition {
    if (!condition) {
    throw new Error('Assertion failed');
    }
    }
  • With that one tweak, our assertion function can narrow types! The next example doesn't cause a type error; the assert(typeof var1 === 'number') call narrows var1's type to number.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const var1: number | string = ((): number | string => 1)();
    assert(typeof var1 === 'number');
    const var2: number = var1;
    var2;
    Result:
    1Pass Icon
  • As always, the narrowed type is enforced, so trying to violate it is a type error.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const var1: number | string = ((): number | string => 1)();
    assert(typeof var1 === 'number');
    const var2: string = var1;
    var2;
    Result:
    type error: Type 'number' is not assignable to type 'string'.Pass Icon
  • Our assert function's type included asserts condition, where condition was an argument to the function. Unfortunately, we can't put an arbitrary boolean expression after asserts; it can only accept a single argument name. For example, we can't write an assertFalse function that declares asserts !condition. That would be a syntax error.

  • (When a code example contains a syntax error, you can answer with syntax error.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    function assertFalse(condition: boolean): asserts !condition {
    if (condition) {
    throw new Error('Assertion failed');
    }
    }
    Result:
    syntax error: on line 7: '{' or ';' expected.Pass Icon
  • Why is this a syntax error and not a type error? A type error happens when the code's structure is legal, but the types don't line up. The ! in the above example is completely illegal: a ! is never allowed after the asserts keyword, regardless of the types. In the example above, the type checker never even gets a chance to run.

  • Assertion functions can also tell the compiler that a value has a specific type. For example, we can write an assertNumber function to simplify the typeof var1 === 'number' checks in our previous examples. Like before, it will narrow the type of its argument.

  • In this example, note the asserts arg is number. That's similar to the syntax that we saw for type predicates in type guard functions. In a type guard function, we'd say arg is number. The difference here is that we add asserts on the front.

  • >
    function assertNumber(n: unknown): asserts n is number {
    if (typeof n !== 'number') {
    throw new Error("Value wasn't a number: " + n);
    }
    }
  • The next few examples show the effect of our assertion function. Be careful; one of them has an intentional type error (you should answer type error), and another one throws a runtime error (you should answer error).

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const n: number | undefined = ((): number | undefined => 5)();
    const n2: number = n;
    n2;
    Result:
    type error: Type 'number | undefined' is not assignable to type 'number'.
      Type 'undefined' is not assignable to type 'number'.Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const n: number | undefined = ((): number | undefined => 5)();
    assertNumber(n);
    const n2: number = n;
    n2;
    Result:
    5Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const n: number | undefined = ((): number | undefined => undefined)();
    assertNumber(n);
    const n2: number = n;
    n2;
    Result:
    Error: Value wasn't a number: undefinedPass Icon
  • Our assertNumber function's argument has the type unknown because it could be any type. The reasoning is the same as it was for type guard functions. We can't make assertNumber take an argument of type number; then it would never do its job of throwing an exception when the argument isn't a number!

  • Here's a code problem:

    Write an assertString assertion function. Make sure that its argument has the type unknown.

    function assertString(s: unknown): asserts s is string {
    if (typeof s !== 'string') {
    throw new Error("Value wasn't a string: " + s);
    }
    }
    const s: string | number = ((): string | number => 'a string')();
    assertString(s);
    const s2: string = s;
    s2;
    Goal:
    'a string'
    Yours:
    'a string'Pass Icon
  • Type guards and assertion functions are very closely related. The difference is in the scope of their type narrowing.

  • With a type guard like if (Array.isArray(x)) { ... }, the type of x is narrowed only inside the ... block. With an assertion function like assertNumber(x), the type of x is narrowed for all code after the assertion call.

  • The compiler has no way to verify our asserts declarations, so it has to blindly trust them, just like it blindly trusts our type guard functions. That makes asserts more dangerous than other parts of the type system.

  • Be careful with this next example; it's intentionally wrong in a way that the type system can't see. The assertNumber function is implemented incorrectly; it accidentally checks for a string type rather than a number type. TypeScript can't see the mistake, so this code compiles and runs, returning a result that violates the stated types.

  • >
    function assertNumber(n: unknown): asserts n is number {
    if (typeof n !== 'string') {
    throw new Error("Value wasn't a number: " + n);
    }
    }
    const s: string = 'a string';
    assertNumber(s);
    const n: number = s;
    n;
    Result:
    'a string'Pass Icon
  • Our suggestion here is the same one that we made for type guard functions. Write some extra automated tests to be sure that your assertions functions do what you think they do.