Skip to content
Theory · Async code and outside data · lesson 9 of 10

Assertions: as and !

At customs the sender writes the parcel's declaration. It can also be forged: you write "books" on a box that holds anything at all. The inspector compares papers, not contents, so he will believe it.

In TypeScript that forgery is the as form. It tells tsc: "treat this value as having this type". tsc will not check the value:

TypeScript
const price: unknown = "100";
const amount = price as number;
console.log(amount.toFixed(2)); // ✗
TEXT
TypeError: amount.toFixed is not a function

tsc is silent: after as number it treats amount as a number. But the variable still holds the string "100", a string has no method toFixed, and the program crashes at run time.

The type assertion

The form value as Type is a type assertion. It changes only what tsc believes and does nothing at run time:

  • the value does not turn into another type: a string stays a string;
  • there is no check at run time: in the JavaScript the line const amount = price as number; becomes const amount = price;;
  • tsc finds no error, and the mistake surfaces at run time, often far from the as.

This is not a conversion like Number(price): as converts nothing. Write it only when you are sure you know more about the value than tsc does. Data from outside needs a real check instead of as: typeof or a type predicate.

The as const form is called an assertion too: a const assertion in tsc's messages. But nothing can be forged with it: it tells tsc nothing new about the value, it only asks it not to widen the literal's types.

When as is rejected

as does not go entirely unchecked. If two types have nothing in common, tsc refuses. For example, it rejects the line const n = "42" as number;:

TEXT
error TS2352: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

The message suggests a way around: assert the type unknown first. The form "42" as unknown as number compiles, but it is a forgery on top of a forgery: n still holds a string. From unknown and any an assertion lets any type through, which is why the lesson's first example passed the check.

The ! operator

The sign ! after an expression is a special kind of assertion: "this is not null and not undefined". It removes them from the type and checks nothing at run time either:

TypeScript
const prices = new Map<string, number>();
prices.set("tea", 120);
const tea = prices.get("tea")!;
console.log(tea.toFixed(2)); // 120.00

Let's go through it line by line:

  1. prices.get("tea") returns number | undefined: the key may be missing.
  2. ! removes undefined, and tea has the type number.
  3. The key "tea" is in prices, so the program prints 120.00.

Without ! the line tea.toFixed(2) would not pass the check: tsc would report TS18048: 'tea' is possibly 'undefined'.

satisfies checks and keeps the type

as has an honest neighbor, satisfies. It compares the value with a type but leaves the value's own type as it was:

TypeScript
type Config = { port: number | string };

const config = { port: 8080 } satisfies Config;
console.log(config.port.toFixed(0)); // 8080

tsc checked that { port: 8080 } fits Config: the form { port: true } satisfies Config would give an error. Yet config.port kept its inferred type number, so toFixed passes. Let's compare the three forms:

  • the annotation const config: Config checks the value and changes the type to Config, and then config.port has the type string | number;
  • satisfies Config checks the value and keeps the inferred type;
  • as Config checks almost nothing and changes the type.

Interviewers like to ask what as does at run time and how it differs from satisfies. The answer: at run time as does nothing, since it is erased along with the types, while satisfies checks the value against a type without changing its inferred type.

Here the forgery analogy falls short in one respect. Forging declarations is against the law, but as is a legitimate tool. You use it when you know more about a value than tsc does and are ready to answer for it.

Common mistake

! is put where there is no value:

TypeScript
const prices = new Map<string, number>();
prices.set("tea", 120);
const coffee = prices.get("coffee")!; // ✗
console.log(coffee.toFixed(2));
TEXT
TypeError: Cannot read properties of undefined (reading 'toFixed')

There is no key "coffee" in prices, get returned undefined, and ! convinced tsc of the opposite. tsc is silent, and the program crashes on the next line. Instead of !, check the value: if (coffee !== undefined).

Key points

  • as changes only what tsc believes: at run time it is erased and converts nothing.
  • Between unrelated types as gives TS2352, and as unknown as gets around the check without changing the value.
  • ! removes null and undefined from the type and checks nothing at run time either.
  • satisfies checks a value against a type and keeps the inferred type.