Comparing satisfies, type annotations, and assertions through compiler output A developer compared TypeScript's `satisfies` operator against type annotations and `as` assertions using emitted declarations and compiler diagnostics under TypeScript 7.0.2. The experiment showed that `satisfies` preserves per-property inference (so `checked.ready` stays `string` and `toUpperCase()` compiles), while annotations and assertions widen values to the target type and, in the assertion case, let a missing required key pass type checking and evaluate to `undefined` at runtime. The writeup also found that `satisfies` rejects excess properties only on inline object literals, not on pre-assigned variables. Use satisfies when a configuration value should meet a type while its individual properties remain convenient to use. Two common shortcuts are misleading, though: that it can never affect inference, and that it always rejects extra keys. The comparison uses the same object under a type annotation, satisfies , and as , with emitted declarations and diagnostics as evidence. For this display configuration, the differences were: | Form | Type when reading ready | Missing waiting | |---|---|---| | : Notices | string | number | | satisfies Notices | string | Error | | as Notices | string | number | The experiment ran on September 11, 2026, with macOS arm64, Node.js 24.15.0, and TypeScript 7.0.2. The compiler used strict: true , target: ES2022 , module: ESNext , and moduleResolution: Bundler , with type checking and declaration emit. The display values are synthetic test data. js type Notices = Record<"ready" | "waiting", string | number ; export const annotated: Notices = { ready: "Ready", waiting: 1, 2 }; export const checked = { ready: "Ready", waiting: 1, 2 } satisfies Notices; export const asserted = { ready: "Ready", waiting: 1, 2 } as Notices; console.log checked.ready.toUpperCase ; checked.ready.toUpperCase passed and printed READY . Calling the same method on annotated.ready produced TS2339 because the property could also be an array. The emitted declarations explain that difference: type Notices = Record<"ready" | "waiting", string | number ; export declare const annotated: Notices; export declare const checked: { ready: string; waiting: number ; }; export declare const asserted: Notices; export {}; checked retains the types of individual properties. The other two variables have the annotated Notices type. This makes satisfies useful when a configuration needs a shared compatibility check without requiring extra narrowing wherever a particular value is used. The TypeScript 4.9 documentation https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html describes this use of compatibility checks while retaining a useful expression type. js type Notices = Record<"ready" | "waiting", string | number ; const value = { ready: "Ready" } as Notices; console.log value.waiting ; This code passed type checking and printed undefined . Changing the assertion to satisfies Notices produced TS2741 for the missing waiting property. An assertion does not create that property. This also does not mean every possible assertion is accepted. TypeScript can reject an assertion based on the relationship between the types. The finding here is narrower: it accepts this particular object despite the missing required key. As the official explanation of type assertions https://www.typescriptlang.org/docs/handbook/2/everyday-types.html type-assertions states, assertions do not validate or convert runtime values. js export const plain = { enabled: true, count: 1 }; export const checked = { enabled: true, count: 1 } satisfies { enabled: boolean; count: number }; export const frozenType = { enabled: true, count: 1 } as const; In the emitted declarations, plain.enabled was boolean , while checked.enabled was true . Both count properties were number . Context from the target type means literal inference is not necessarily identical. With as const , frozenType.enabled became a readonly true , and frozenType.count became a readonly 1 . This does not freeze the object at runtime either. If a configuration will be modified later, decide whether that property should remain the literal true or accept both boolean values, including false . Writing { ready: "Ready", waiting: 1 , typo: 1 } satisfies Notices directly produced TS2353 for the extra typo property. Assigning that object to a variable first changed the outcome: js type Notices = Record<"ready" | "waiting", string | number ; const input = { ready: "Ready", waiting: 1 , typo: 1 }; const value = input satisfies Notices; console.log value.typo ; This passed and printed 1 . A value can satisfy the required structure while retaining additional properties. Restricting the keys of external data requires a runtime check separate from these type constructs. For the local configuration in this experiment, use satisfies to catch mistakes while retaining operations specific to each value. Use an annotation to specify the type the variable should expose. Reserve as for places where other evidence supports the asserted type. The T02 cases in the reproduction code https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/experiments/article-stock-2026-09/language-batch08/compiler-cases.json and the execution record https://github.com/takahiro-saeki/articles/blob/codex/article-stock-2026-09/production/2026-09/batch-08/compiler-results.json include both accepted declarations and deliberately invalid inputs. Do not assume diagnostic numbers or inference remain identical across compiler versions; compare declaration output when upgrading.