🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsHeader and Footer not showing in Excel(14.09.2026 um 22:43 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)
🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🪟 Windows TippsHeader and Footer not showing in Excel(14.09.2026 um 22:43 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
🪟 Windows TippsKB5129194 Windows 11 26H1 Out of Band Update - Deskmodder.de(14.09.2026 um 19:25 Uhr)

🔧 Programmierung 🕛 vor 3 Monaten 7 Min Lesezeit
0

satisfies vs Type Annotation: The TypeScript Choice That Changes Inference

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht


  • Book:


  • My project: — an IDE for developers who ship with Claude Code and other AI coding tools


  • Me:






You have a config object. You want the compiler to check it

against a known shape, so you reach for a type annotation:




CODE
type RouteConfig = Record<string, string>;

const routes: RouteConfig = {
home: "/",
users: "/users",
billing: "/billing",
};






The check works. Add a route whose value is a number and the

compiler complains. Good. Then you try to read a route by its

key and the editor goes quiet:




CODE
routes.home;
// string

routes.dashboard;
// string — no error, even though "dashboard"
// is not in the object






The annotation did its job and then erased everything the

compiler had figured out on its own. routes is now exactly

Record<string, string>. The literal keys are gone. The literal

values are gone. You traded inference for a check.



satisfies is the keyword that lets you keep both. It checks

the value against the type without throwing away the narrow type

the compiler inferred. It landed in TypeScript 4.9 in late 2022,

and config objects are where it earns its place.





What the annotation actually does



A type annotation is a contract in one direction: the value must

be assignable to the declared type, and from that point on the

binding is the declared type. The compiler forgets the

specifics of the literal you wrote.




CODE
const colors: Record<string, string> = {
primary: "#1a1a1a",
accent: "#d97b2b",
};

type Keys = keyof typeof colors;
// string — not "primary" | "accent"






keyof typeof colors is string, because as far as the type

system is concerned colors is a Record<string, string> with

arbitrary string keys. The two keys you wrote are an

implementation detail the annotation discarded.



That is fine when you want it. A function parameter typed as

Record<string, string> should accept any such record. But for

a config object you read from, the lost detail is the whole

point of having it in source.





What satisfies does instead



satisfies checks the expression against a type and then leaves

the inferred type in place. Same check, no widening.




CODE
const colors = {
primary: "#1a1a1a",
accent: "#d97b2b",
} satisfies Record<string, string>;

type Keys = keyof typeof colors;
// "primary" | "accent"

colors.primary;
// "#1a1a1a" — the literal, not string

colors.missing;
// error: Property 'missing' does not exist






The constraint Record<string, string> still runs. Put a

number where a string belongs and you get the same error you

would with an annotation:




CODE
const broken = {
primary: "#1a1a1a",
accent: 0xd97b2b,
} satisfies Record<string, string>;
// error: Type 'number' is not assignable to type 'string'.






So you get the guard rail and you keep the narrow type. The key

names are real. The values are literals. Autocomplete works.

Misspelled lookups fail.





The config-object case where it matters



Here is a shape you have written some version of. A map from a

known set of environments to their settings.




CODE
type EnvConfig = {
apiUrl: string;
timeoutMs: number;
retries: number;
};

const config = {
dev: { apiUrl: "http://localhost", timeoutMs: 1000, retries: 0 },
prod: { apiUrl: "https://api.app", timeoutMs: 5000, retries: 3 },
} satisfies Record<string, EnvConfig>;






Each entry is checked against EnvConfig. Forget retries in

one of them and the compiler points at that entry. That is the

annotation half of the job, and satisfies does it.



Now the part an annotation throws away. Because the inferred

type survives, you can derive from it:




CODE
type Env = keyof typeof config;
// "dev" | "prod"

function load(env: Env) {
return config[env];
}

load("dev"); // ok
load("staging"); // error: not "dev" | "prod"






Env is the union of the actual keys. Had you annotated

config as Record<string, EnvConfig>, keyof typeof config

would be string and load("staging") would compile and blow

up at runtime when it indexed an object that has no staging

key. The narrow type the compiler kept is what makes the lookup

safe.





Where the annotation still wins



satisfies is not a replacement for annotations. They answer

different questions.



An annotation says: this binding has this type, treat it that

way everywhere.
That is what you want for a public API surface,

where the declared type is the thing you are promising and the

literal happens to be how you fill it today.




CODE
export const defaults: Settings = {
theme: "dark",
fontSize: 14,
};






Here you want defaults to be Settings, not the narrow shape.

A consumer should see the documented type, and you should be

free to reassign or build defaults differently later without

the inferred type rippling out. The annotation is the right

call.



Annotations also catch a class of error satisfies does not:

the missing property at the point of declaration.




CODE
const a: Settings = { theme: "dark" };
// error: Property 'fontSize' is missing

const b = { theme: "dark" } satisfies Settings;
// error: Property 'fontSize' is missing






Both flag it here, so that is a wash. The real split is

direction. An annotation forces the value down to the type.

satisfies checks the value against the type and keeps the

value's own type. When you read from the binding, you almost

always want the second. When you hand the binding to someone

else as a typed contract, you usually want the first.





Combining the two



You can use both when you want a checked, narrow value behind a

declared contract. The order reads outside-in: annotate the

exported name, build it with satisfies so the internal lookups

stay sharp.




CODE
const internalRoutes = {
home: "/",
users: "/users",
} satisfies Record<string, string>;

// internal code gets "home" | "users" autocomplete
internalRoutes.home;

// exported surface is the wide, documented type
export const routes: Record<string, string> = internalRoutes;






Inside the module you keep the literal keys. The export presents

the contract. Two bindings, two jobs, no lost information in

either.





The one place satisfies bites



satisfies checks excess properties the way an object literal

does, which can surprise you when the constraint is a union.




CODE
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number };

const s = {
kind: "circle",
r: 10,
side: 4,
} satisfies Shape;
// error: Object literal may only specify known
// properties, and 'side' does not exist in
// type ...






The annotation form rejects this too, so the behavior is

consistent. The thing to remember is that satisfies does not

loosen any check. It only changes what type the binding ends up

with. If a value fails an annotation, it fails satisfies with

the same message.






The rule of thumb



Reach for satisfies when you read from the value and want the

compiler to remember the specifics: config maps, route tables,

lookup objects, anything where keyof typeof or a literal value

type is useful downstream. Reach for an annotation when the

declared type is the contract you are exposing and the literal

is just today's filling.



The mistake is using an annotation on a config object you read

from, watching autocomplete go dark, and assuming that is the

cost of type safety. It is not. satisfies gives you the check

and keeps the inference. On a config object, that is the whole

game.






If the difference between widening and constraint inference is

the kind of distinction you want laid out in full — generics,

conditional and mapped types, infer, the machinery satisfies

sits on top of — that is what The TypeScript Type System digs

into. The config-object patterns here are the shallow end of it.



The TypeScript Library — the 5-book collection. Books 1 and 2 are the core path; 3 and 4 substitute for 1 and 2 if you come from the JVM or PHP; book 5 is for anyone shipping TS at work.





  1. — generics, mapped/conditional types, infer, template literals, branded types.


  2. — the sync-to-async shift, generics, discriminated unions for PHP 8+ developers.


  3. Vollständiger Original-Bericht
    Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
    ↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
The Gemini desktop app is now available for Windows
1 Quelle
Header and Footer not showing in Excel
1 Quelle
Burn Out, Or Fade Away
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten satisfies vs Type Annotation: The TypeScript Choice That Changes Inference

Thematisch verwandte Begriffe: satisfies, Type, Annotation, TypeScript · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...