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 throughunknownto force a conversion the compiler would refuse.- Non-null assertion
- The postfix
!that removesnullandundefinedfrom 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
stringinstead 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
satisfiesor 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.
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 runtime02The 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.
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.
const stock = new Map<string, number>();
const bad = stock.get("pen")! + 1; // NaN at runtime, no warning
const good = (stock.get("pen") ?? 0) + 1; // 104satisfies: 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.
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 string05Pick the tool
The flow below is the order of preference. Every step to the left is safer than the one to its right.
Try it yourself
- 1
Fix the lie
In the first example, replace
const n = raw as number;withconst n = typeof raw === "number" ? raw : 0;. Predict the runtime result:n.toFixed(1)prints0.0. The check turns a runtime crash into a safe default. - 2
Swap satisfies for an annotation
In the third example, change
colorstoconst colors: Palette = .... Predict which line breaks: theconsole.logthat calls.mapand.toUpperCaseon properties that are now the union type. - 3
Spot the typo
Add a field named
grenwith value"#00ff00"tocolors. WithPaletteusing 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 withsatisfies.
Code & diagrams
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 functionWithout 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
NaNcolors 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 ] #00FF00Break 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;.
Break #2
Assert a type that isn't true
Keep const raw: unknown = "hello"; and write const n = raw as number; n.toFixed(1);.
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 TandT as unknownalways work, and why asserting fromstringto"a" | "b"works (the literal union is assignable tostring). - ▸
satisfiesalso provides contextual typing: inside the object literal, callbacks and nested values get types from the target, so{ onClick: (e) => ... } satisfies Handlerstypesecorrectly, while a plain annotation-freeconstcould 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 staysstring; combine it withas const({ ... } as const satisfies T) to keep literals and check the contract. - ▸
Prefer assertion functions and type predicates (Topic 4.7) over
aswhen 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 thex is Tclaim. - ▸
Lint rules help:
@typescript-eslint/no-non-null-assertionflags!, andno-unnecessary-type-assertionflags assertions that the compiler already knows. Team conventions often say:asand!allowed only with a comment,as unknown asneeds review.
Remember this
- 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 aboutvaluefrom 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.tsxfiles, so teams standardise onas. - 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 numberis an error (TS2352: the types do not sufficiently overlap). A double assertion goes throughunknownto 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
**
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 becomereadonly. Nothing here can be wrong, so it is the one assertion you can use freely. - 4
The **non-null assertion
!**, written after an expression (map.get("pen")!), tells the compiler "this isn'tnullorundefined" 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 asNaN,undefinedis not a function, orCannot read properties of undefined. In the live examplestock.get("pen")!followed by+ 1quietly givesNaN. Prefer a real check (if (x !== undefined)) or a default (x ?? 0). - 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.redbecomes the whole unionstring | number[]. An assertion,{...} as Palette, skips checking.satisfies Palettechecks and keepscolors.redasnumber[]andcolors.greenasstring, so you can call.mapon the first and.toUpperCase()on the second. - 6
satisfiesis the right tool for configuration objects and lookup tables where you want typos and wrong values caught (blue: trueis 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
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 useunknownplus narrowing rather than an assertion. Usesatisfiesto check a literal against a contract. Useas constfreely. Useasand!last, narrowly, and only when you can say in a comment why they are true.
Explain it without notes
What does value as Type do at runtime?
When is a double assertion needed, and why is it a warning sign?
What does the ! operator do, and what are the alternatives?
What is satisfies, and how does it differ from a type annotation and from as?
Give a case where you would use satisfies and a case where an annotation is better.
Practice
Define const routes = { home: "/", about: "/about" } so that every value must be a string, the keys stay known, and routes.contact is an error.
Replace the ! in stock.get("pen")! + 1 with code that is correct when the key is missing.
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
asand!in a codebase is a small debt. - ↔
satisfiesgives 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
asand!are erased and change only what the compiler believes.I can say why
"text" as numberis an error and what a double assertion does.I can replace a
!with a check,?.or??.I can use
as constfreely and explain what it keeps.I can use
satisfiesto check an object against a type and keep its precise type.