TypeScript's `satisfies readonly T[]`: What It Checks—and What It Doesn't
This TypeScript expression looks as if it creates a read-only array:
1
2
3
4
5
6
7
8
9
10
11
12
13
type MarketStatus = "open" | "paused" | "closed" | "resolved";
type Market = {
id: string;
status: MarketStatus;
};
const markets = [
{
id: "market_1",
status: "open",
},
] satisfies readonly Market[];
It does not.
The expression checks that markets is compatible with readonly Market[], while preserving the type TypeScript inferred for the array. That distinction is useful, but easy to miss.
What satisfies Actually Does
The satisfies operator asks TypeScript to verify that an expression can be assigned to a target type without replacing the expression’s inferred type with that target.
It catches invalid data:
1
2
3
4
5
6
7
const markets = [
{
id: "market_1",
status: "running",
// Type '"running"' is not assignable to type 'MarketStatus'.
},
] satisfies readonly Market[];
It also catches missing required fields and, for object literals, unexpected properties in the relevant contextual type.
The useful mental model is:
1
2
: Type use Type as the variable's declared type
satisfies Type verify compatibility, then keep the inferred type
That preserved inference is the main reason to use satisfies for configuration objects and static data.
Why readonly Does Not Make This Array Read-Only
The target type says that a read-only consumer is allowed to receive the array. A mutable array is compatible with that promise: code that only reads it will not mutate it.
But satisfies does not change the array’s inferred type. In this example, the variable itself is still mutable:
1
2
3
4
5
const markets = [
{ id: "market_1", status: "open" },
] satisfies readonly Market[];
markets.push({ id: "market_2", status: "closed" }); // allowed
This behavior follows directly from the two parts of the expression:
readonly Market[]is the compatibility check.satisfiesavoids replacing the inferred type withreadonly Market[].
So the code expresses “this value is safe to pass where a read-only market array is expected,” not “this variable cannot be mutated.”
Three Similar-Looking Choices
1. A type annotation
Use an annotation when the variable should be treated as a read-only array throughout the rest of the program:
1
2
3
4
5
6
const markets: readonly Market[] = [
{ id: "market_1", status: "open" },
];
markets.push({ id: "market_2", status: "closed" });
// Property 'push' does not exist on type 'readonly Market[]'.
The trade-off is that each element is viewed through the broader Market type. More specific information from the literal may be widened.
2. satisfies alone
Use satisfies when you want validation while retaining useful inference:
1
2
3
const markets = [
{ id: "market_1", status: "open" },
] satisfies readonly Market[];
This validates the data but does not make the inferred array read-only.
3. as const satisfies
Use a const assertion when the literal itself should retain deeply specific, read-only literal types:
1
2
3
4
5
6
const markets = [
{ id: "market_1", status: "open" },
] as const satisfies readonly Market[];
markets.push({ id: "market_2", status: "closed" });
// Property 'push' does not exist on the readonly tuple.
Now TypeScript infers a read-only tuple whose values remain literal types, approximately:
1
2
3
4
5
6
readonly [
{
readonly id: "market_1";
readonly status: "open";
},
]
This is powerful for fixed configuration, but it can be unnecessarily restrictive for data that is expected to change.
readonly Is Still Compile-Time Only
None of these TypeScript forms freezes a JavaScript object at runtime. Type annotations are erased when TypeScript is compiled.
If runtime immutability matters, use a runtime mechanism such as Object.freeze—and remember that Object.freeze is shallow unless you recursively freeze nested values.
1
2
3
const markets = Object.freeze([
Object.freeze({ id: "market_1", status: "open" as const }),
]);
Runtime immutability and compile-time read-only types solve related but different problems.
A Practical Decision Guide
Choose based on the guarantee the code actually needs:
| Goal | Prefer |
|---|---|
| Validate static data and preserve inference | value satisfies Type |
| Expose an array through a read-only variable | const value: readonly T[] |
| Keep a fixed literal deeply specific and read-only | value as const satisfies Type |
| Prevent mutation at runtime | Object.freeze or another runtime strategy |
For a fixed list of seed markets or application configuration, this is often a good fit:
1
2
3
4
export const markets = [
{ id: "market_1", status: "open" },
{ id: "market_2", status: "paused" },
] as const satisfies readonly Market[];
For a list that will grow while the application runs, use a mutable array and expose read-only views at API boundaries instead of forcing the source data into a const assertion.
The Short Version
satisfies readonly Market[] means:
Check that this value can be used as a read-only array of markets, while preserving its inferred type.
It does not mean:
Turn this value into a read-only array.
That small distinction is exactly why satisfies is useful—and why it can be surprising.