Lesson 3 · Module 0.3 — Reading TS syntax

Widen, assert, or check: annotation vs as vs satisfies

There 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.

The one distinction

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.

A worked example

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.

The comparison table

syntaxcan add props later?keeps literal types?use case
const o: Type = …yesnoadd properties later
const o = … as Typeyesnoadd properties later
const o = … satisfies Typenoyescheck 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).

Why satisfies keeps the narrow type

satisfies 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.

Check yourself

Recall, don’t re-read. (Answers reveal on click.)

After const c = {} satisfies Record<string, number>;, why is c.x = 1 an error?
Which one performs no real check and will happily compile a false claim?
You want each value's exact literal type kept and the shape validated. Use…

Primary source

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.

Try it yourself

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.