September 23, 2022·4 min read·engineeringtypescriptsatisfiesinference

TypeScript 4.9 beta checks a shape without replacing its useful type

The satisfies operator checks compatibility while retaining an expression's inferred information. Its contextual typing and compile-time scope matter when using it for configuration objects.

A configuration object often needs two kinds of type information. Its author wants a check against a shared contract, while its consumers need the more specific information present in the actual values. A broad annotation can satisfy the first requirement while discarding useful distinctions for the second. TypeScript 4.9's beta introduces an operator for expressing those requirements together.

The September 23 announcement presents satisfies through an object whose values can be strings or numeric tuples. The checker should catch a misspelled key without making every correctly spelled property's value look like the whole union. That is a concrete inference problem in ordinary application configuration.

I find TypeScript most useful when a contract improves local code without forcing readers through extra assertions. This operator lets an object declaration state what it must satisfy while leaving consumers access to information specific to that object. The distinction is particularly useful for values whose permitted forms require different operations.

An annotation exposes the broad contract

Suppose a palette permits either a textual color or a numeric tuple. Annotating the whole object as a record of that union checks its keys and values. Subsequent property access then sees the annotated union, even when the initializer supplied an unambiguous string.

Using a string-only method consequently requires narrowing the value again. The annotation has deliberately exposed the contract rather than the initializer's individual property types. That may be appropriate for a public variable whose values will change, but it can be inconvenient for a fixed configuration declaration.

The new operator checks the same compatibility requirement while retaining the expression's useful property information. A string-valued member can keep its string operations, while the tuple member retains its corresponding structure.

type Color = "red" | "green" | "blue"
type RGB = [number, number, number]

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
} satisfies Record<Color, string | RGB>

const normalized = palette.green.toUpperCase()

The example uses a method specific to strings to show the preserved distinction. It does not depend on an array operation shared with strings. Misspelling a required color key or supplying an incompatible value would still produce a diagnostic at the declaration.

Context participates in inference

The implementation patch shows both parts of the operation. It obtains the target type and checks expression compatibility with it, then returns the expression's type. The target also provides contextual typing to the operand.

That last detail limits a common shorthand explanation. Saying the operator preserves inference does not mean the target has no influence on how an object literal is inferred. Context can supply information for the expression while the result avoids being replaced wholesale with the target type.

The patch's tests include literal-valued properties and callback parameters. Those cases show why contextual information matters. A callback can need a parameter type supplied by the expected shape, and a literal alternative can influence how narrowly a property is represented.

The useful distinction is therefore between checking against a contract and exposing that entire contract as the result. The operator performs the former while retaining the checked expression's own type. It should not be treated as a guarantee that removing it leaves every inferred detail identical.

The key contract is a deliberate choice

The announcement demonstrates checking a literal object against a record with a finite key union. This catches extra keys in that declaration and requires the listed keys. A configuration that must cover every named category can express that expectation directly.

A string-indexed record expresses another requirement. It permits arbitrary string keys while checking their values against the selected value type. That is appropriate when a configuration's vocabulary is open, but it cannot catch an unexpected key by comparing it with a finite list.

These are choices made by the target type. The operator cannot infer whether a domain intended an open dictionary or a closed list of categories. Selecting the contract determines which errors the declaration can expose.

I prefer to keep that distinction visible in configuration types. A finite key union communicates an application's recognized vocabulary. A string index signature communicates that consumers must handle names beyond a predetermined set.

Compatibility checking does not validate external data

The operator is a compile-time construct. Its implementation participates in TypeScript checking and transformation, not a runtime schema check over incoming values. The expression remains the executable value after the type syntax is removed.

That scope makes the feature useful for source-authored configuration without changing the treatment of network responses or parsed files. Those inputs still need runtime checks when their actual values are unknown. A static compatibility claim cannot inspect bytes that only arrive during execution.

The beta offers a direct way to evaluate this declaration pattern. A contract can reject an incorrect source initializer while consumers retain the specific member types established by that initializer. The resulting code keeps the check at the declaration where the configuration is authored, instead of requiring each consumer to recover information that a broad annotation discarded.