Lesson 3 · Module 0.3 — Reading TS syntax
as vs satisfiesThere are three ways to attach a type to an object literal —
: Type, as Type, and satisfies Type. They look
interchangeable. They are not. Two of them widen the variable to the declared
type; one only checks the value and keeps the narrow type you wrote. Reading the
keyword tells you which type the variable actually has.
Annotation and as make the variable's type the wide declared
type — you can add matching props later, but you lose the literal detail. satisfies
verifies the value against the type, then keeps the narrow
type of the literal you actually wrote. Wide lets you grow; narrow remembers exactly what's there.
Take one object, attached three ways to Record<string, number>. Watch what each
keyword does to the variable's type afterward:
// ANNOTATION — variable type is the WIDE Record; you may add any string→number prop
const a: Record<string, number> = {};
a.x = 1; // OK — the box accepts any string key with a number value
// as — same WIDE type, by assertion this time; same freedom to add props
const b = {} as Record<string, number>;
b.x = 1; // OK — b is typed Record, so new keys are allowed
// satisfies — CHECKS {} against Record, then keeps the NARROW literal type {}
const c = {} satisfies Record<string, number>;
c.x = 1; // ERROR — c is still {}, an empty object; "x" was never declared on it
The first two say “this variable is a Record” — so the compiler lets you keep adding
number-valued keys. The third says “confirm this value fits Record, but remember it's exactly
{}” — so adding x later is rejected, because {} has no
x. The keyword, not the type, decides.
| syntax | can add props later? | keeps literal types? | use case |
|---|---|---|---|
| const o: Type = … | yes | no | add properties later |
| const o = … as Type | yes | no | add properties later |
| const o = … satisfies Type | no | yes | check a literal at creation |
Read it as one rule: annotation and as give you the wide declared type
(room to grow, literal detail lost); satisfies gives you a checked value at its
narrow literal type (no room to grow, every detail kept).
satisfies keeps the narrow typesatisfies is a check, not a type change. It asks one question at the
moment you write the value — “does this fit the rule?” — and then steps out of the way, leaving the
variable with the precise type it already had. That's why it's the right tool when you want the
literal detail and a guarantee the shape is valid:
const config = {
host: "localhost",
port: 8080,
} satisfies Record<string, string | number>;
config.port.toFixed(); // OK — port is still the literal 8080 (a number), so number methods work
config.host.toUpperCase();// OK — host is still "localhost" (a string), method is available
Had you annotated : Record<string, string | number> instead, every value would widen
to string | number — and config.port.toFixed() would fail, because
string | number has no .toFixed. satisfies validates the shape
yet keeps each value's exact type. That is its entire reason to exist.
as can lie — satisfies cannot
as is an assertion: it tells the compiler “trust me” and performs no real
check, so a wrong as compiles happily and explodes at runtime
(const n = "hi" as unknown as number — now n.toFixed() crashes).
satisfies can't lie: it only verifies, so a value that doesn't fit is a
compile error, never a silent runtime bomb. When you read as, stay suspicious; when
you read satisfies, the check already happened.
Recall, don’t re-read. (Answers reveal on click.)
const c = {} satisfies Record<string, number>;, why is c.x = 1 an error?The official handbook page covering the satisfies operator — and how it differs from a
type annotation that would widen your values — is
TypeScript 4.9 — The satisfies Operator.
For why as is an unchecked assertion, see
Everyday Types — Type Assertions.
Best way to feel it: paste all three {} variants into the
TS Playground and hover each variable to compare its type.
Got a real as in your code and not sure it's safe? Read it closely and ask “is this
assertion a lie?” Next lesson (0.4) builds on this: structural typing & excess-property
checks — why an inline object errors but the same object in a variable slides right through.