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
errorif an example will cause a thrown error at runtime. When thisassertfunction doesn't throw an error, it will returnundefined.)>
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:
undefined
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
assert(1 + 1 === 3);Result:
Error: Assertion failed
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
var1has the typenumber | string. What happens if we callassert(typeof var1 === 'number')? The code after that point will only run ifassertdidn't throw an error. That means thatvar1must've actually been a number. But TypeScript doesn't know that; it still thinks that it's anumber | 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'.
Remember that we can narrow types by using
if (typeof x === ...). In the next example, we narrow a variable's type fromnumber | stringto justnumber. The TypeScript compiler allows this because it knows thatvar1has the typenumberinside theif.- 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:
1
TypeScript 3.7 allows assertion functions to narrow types, kind of like how type guards narrow them. It requires one small tweak to the
assertfunction. We tell the TypeScript compiler what we already know: if theassertfunction returns at all, then the condition that we passed to it must be true.In this example, note the new
asserts conditionsyntax. It's referring tocondition, 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 narrowsvar1's type tonumber.- 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:
1
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'.
Our
assertfunction's type includedasserts condition, whereconditionwas an argument to the function. Unfortunately, we can't put an arbitrary boolean expression afterasserts; it can only accept a single argument name. For example, we can't write anassertFalsefunction that declaresasserts !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. 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 theassertskeyword, 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
assertNumberfunction to simplify thetypeof 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 sayarg is number. The difference here is that we addassertson 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 answererror).- 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'.
- 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:
5
- 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: undefined
Our
assertNumberfunction's argument has the typeunknownbecause it could be any type. The reasoning is the same as it was for type guard functions. We can't makeassertNumbertake an argument of typenumber; 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
assertStringassertion function. Make sure that its argument has the typeunknown.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'
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 ofxis narrowed only inside the...block. With an assertion function likeassertNumber(x), the type ofxis narrowed for all code after the assertion call.The compiler has no way to verify our
assertsdeclarations, so it has to blindly trust them, just like it blindly trusts our type guard functions. That makesassertsmore 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
assertNumberfunction 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'
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.