Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner ~27 min· topic 6 of 6

Topic 5.6

Type Assertions and the satisfies Operator

In one line

A type assertion (as, !) tells the compiler "trust me, I know the type" and the compiler stops checking; satisfies does the opposite and checks a value against a type without changing what TypeScript knows about it.

Think of it like this

A security guard checks tickets at a gate. If you say "I'm with the band, let me through", an assertion is the guard believing you and waving you in with no check. A **satisfies** check is the guard looking at your ticket to confirm it's valid, then letting you in *and still remembering exactly which seat is yours*. **An assertion overrides the checker with your word. satisfies makes the checker prove it.**

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Type assertion
Telling the compiler a value has a certain type, with as. It is trusted, not checked.
Double assertion
x as unknown as T: going through unknown to force a conversion the compiler would refuse.
Non-null assertion
The postfix ! that removes null and undefined from a type without checking.
satisfies
An operator that checks a value against a type but keeps the value's own precise type.
Widening
When the compiler picks a broader type, such as string instead of "dark".
Annotation
A type you write after a name, like const x: Palette. It replaces the inferred type.
Contract
The type a value must follow, which satisfies or an annotation checks.
Sufficient overlap
The rule for as: one of the two types must be assignable to the other.

Step by step

01as: you override the checker

raw below is unknown: it could be anything. Writing raw as string makes the compiler treat it as text from then on. If you were right, great. If raw was a number, the compiler won't warn you, and the failure will come later in a place that looks innocent.

as.tswhole filets
const raw: unknown = "hello";
const len = (raw as string).length; // 5, but only because you were right
const n = raw as number;            // compiles, and is a lie
n.toFixed(1);                       // TypeError at runtime

02The compiler refuses nonsense, you can force it

"text" as number is rejected because a string and a number can never be the same value. Going through unknown is the escape hatch. Treat it as a flashing warning light.

double.tswhole filets
const a = "text" as number;           // error TS2352
const b = "text" as unknown as number; // compiles. Now b + 1 is the text "text1"

03! hides bugs; checks reveal them

Map.get returns number | undefined because the key might be missing. ! throws that fact away. A check or ?? keeps it.

bang.tswhole filets
const stock = new Map<string, number>();
const bad = stock.get("pen")! + 1;       // NaN at runtime, no warning
const good = (stock.get("pen") ?? 0) + 1; // 1

04satisfies: check, don't forget

With satisfies, TypeScript compares the object to Palette and reports a mistake, then keeps the real type of each property. With : Palette it would have forgotten that red is an array.

satisfies.tswhole filets
type Palette = { [name: string]: string | number[] };
const colors = { red: [255, 0, 0], green: "#00ff00" } satisfies Palette;
colors.red.map((n) => n / 255);  // ok: red is number[]
colors.green.toUpperCase();      // ok: green is string

05Pick the tool

The flow below is the order of preference. Every step to the left is safer than the one to its right.

Pick the tooldiagram
Rendering diagram…

Try it yourself

  1. 1

    Fix the lie

    In the first example, replace const n = raw as number; with const n = typeof raw === "number" ? raw : 0;. Predict the runtime result: n.toFixed(1) prints 0.0. The check turns a runtime crash into a safe default.

  2. 2

    Swap satisfies for an annotation

    In the third example, change colors to const colors: Palette = .... Predict which line breaks: the console.log that calls .map and .toUpperCase on properties that are now the union type.

  3. 3

    Spot the typo

    Add a field named gren with value "#00ff00" to colors. With Palette using an index signature it compiles (any key is allowed). Now think about how a type with a fixed list of keys would catch the typo; Topic 10.5 shows this with satisfies.

Code & diagrams

as: the compiler believes you, even when you are wrong

raw as string is true here, so len is 5. raw as number compiles and then fails at runtime, with a message that has nothing to do with the assertion. The direct conversion of text to number is refused, but the double assertion goes through; t is typed number while it really holds the text.

Console

(nothing was printed)

Expected console output

5 text
n.toFixed is not a function
as const and the non-null assertion

Without as const, the properties are widened to string and number. With it they keep the exact values and become readonly. stock.get returns number | undefined; the ! removes undefined from the type, and the lie shows up as NaN, with no error anywhere. (The <string, number> part is a generic argument, Phase 7: it says the map holds numbers under string keys.)

Console

(nothing was printed)

Expected console output

NaN
satisfies versus an annotation TypeScript 4.9+

colors keeps the exact property types, so .map and .toUpperCase both work. The annotated version only knows the broad union (and, with noUncheckedIndexedAccess, that the key might be missing), so the same call fails. The last line shows satisfies doing its checking job: blue: true is not a string or number list.

Console

(nothing was printed)

Expected console output

[ 1, 0, 0 ] #00FF00

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Assert unrelated types

Write const s = "text" as number;.

terminal
$ Types panel
── what you'll see ──
index.ts(5,11): 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.

Break #2

Assert a type that isn't true

Keep const raw: unknown = "hello"; and write const n = raw as number; n.toFixed(1);.

terminal
$ Console
── what you'll see ──
n.toFixed is not a function

Myth vs fact

Myth

as converts the value to the new type.

Fact

It converts nothing. It only changes what the compiler believes. "5" as unknown as number is still the text "5" at runtime.

Myth

! is safe because TypeScript will check it later.

Fact

TypeScript never checks it. ! is erased, and a wrong one fails somewhere else with NaN or undefined is not a function.

Myth

satisfies is just another way to write an annotation.

Fact

An annotation replaces the value's type with the declared one. satisfies only checks against it and keeps the value's own, narrower type.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Assertions are checked by comparability, not assignability: the source type or the target type must be assignable to the other (roughly). That is why unknown as T and T as unknown always work, and why asserting from string to "a" | "b" works (the literal union is assignable to string).

  • ▸

    satisfies also provides contextual typing: inside the object literal, callbacks and nested values get types from the target, so { onClick: (e) => ... } satisfies Handlers types e correctly, while a plain annotation-free const could not. This is what makes it useful for routing tables and configuration objects. On a literal like { mode: "dark" } satisfies { mode: string }, the property type stays string; combine it with as const ({ ... } as const satisfies T) to keep literals and check the contract.

  • ▸

    Prefer assertion functions and type predicates (Topic 4.7) over as when validating at a boundary: they put the check and the type change in the same place, and if the check is wrong, the mistake is in one function rather than at each call site. Even then, the predicate body is only as good as your code, because TypeScript trusts the x is T claim.

  • ▸

    Lint rules help: @typescript-eslint/no-non-null-assertion flags !, and no-unnecessary-type-assertion flags assertions that the compiler already knows. Team conventions often say: as and ! allowed only with a comment, as unknown as needs review.

Remember this

  1. 1

    The problem this solves: Sometimes you know more than the compiler, and sometimes you want to check a value against a type without losing its precise type. The wrong tool hides bugs. A type assertion is written value as Type. It does not convert anything: no code runs and nothing is checked at runtime. It only changes what the compiler believes about value from that point on. Assertions are useful when you know more than the compiler (for example, after you have validated data in a way TypeScript can't see), and dangerous in every other case. Another spelling, <Type>value, exists but can't be used in .tsx files, so teams standardise on as.

  2. 2

    TypeScript still stops you from asserting something obviously wrong. You may assert from a type to a more specific or less specific type (unknown as string, Dog as Animal), but "text" as number is an error (TS2352: the types do not sufficiently overlap). A double assertion goes through unknown to bypass that: "text" as unknown as number. It always compiles, and it is a clear sign you are lying to the compiler, so every use deserves a comment saying why it is safe.

  3. 3

    **as const** (Topic 4.2, Topic 5.1) is a different, safe assertion. It does not claim a type; it asks the compiler to keep the narrowest type: literal values stay literal, arrays become readonly tuples, and object properties become readonly. Nothing here can be wrong, so it is the one assertion you can use freely.

  4. 4

    The **non-null assertion !**, written after an expression (map.get("pen")!), tells the compiler "this isn't null or undefined" and removes those from the type. It is erased at runtime. If you were wrong, you get no error at the assertion; the bug appears later as NaN, undefined is not a function, or Cannot read properties of undefined. In the live example stock.get("pen")! followed by + 1 quietly gives NaN. Prefer a real check (if (x !== undefined)) or a default (x ?? 0).

  5. 5

    **satisfies (TypeScript 4.9) checks that a value is compatible with a type, and keeps the value's own, more precise type**. Compare three ways to describe a colour table. An annotation, const colors: Palette = {...}, checks the shape but throws away the details: colors.red becomes the whole union string | number[]. An assertion, {...} as Palette, skips checking. satisfies Palette checks and keeps colors.red as number[] and colors.green as string, so you can call .map on the first and .toUpperCase() on the second.

  6. 6

    satisfies is the right tool for configuration objects and lookup tables where you want typos and wrong values caught (blue: true is rejected by the live example) yet still want precise types and autocomplete on the keys you wrote. It catches excess keys in the object literal too, when the target type has a fixed list of keys. It doesn't change the runtime output at all.

  7. 7

    How to decide: first try to let inference or a check do the job (a type guard, if, ??). If a value really comes from outside the type system, validate it, and use unknown plus narrowing rather than an assertion. Use satisfies to check a literal against a contract. Use as const freely. Use as and ! last, narrowly, and only when you can say in a comment why they are true.

Explain it without notes

01

What does value as Type do at runtime?

02

When is a double assertion needed, and why is it a warning sign?

03

What does the ! operator do, and what are the alternatives?

04

What is satisfies, and how does it differ from a type annotation and from as?

05

Give a case where you would use satisfies and a case where an annotation is better.

Practice

01

Define const routes = { home: "/", about: "/about" } so that every value must be a string, the keys stay known, and routes.contact is an error.

02

Replace the ! in stock.get("pen")! + 1 with code that is correct when the key is missing.

03

Explain what "42" as unknown as number does when you add 1 to it, and write the correct conversion.

Trade-offs

  • ↔

    Assertions are fast to write and sometimes the only option (for example, a DOM element you know exists), but each one is a spot where the compiler can no longer help you. Every as and ! in a codebase is a small debt.

  • ↔

    satisfies gives both safety and precision but needs TypeScript 4.9 or later, and some tools and syntax highlighters lag behind. All current versions (6.0 and 7.0) support it.

Done when you can

  • I can explain that as and ! are erased and change only what the compiler believes.

  • I can say why "text" as number is an error and what a double assertion does.

  • I can replace a ! with a check, ?. or ??.

  • I can use as const freely and explain what it keeps.

  • I can use satisfies to check an object against a type and keep its precise type.