satisfies
Checks an expression against a type without widening the expression's own inferred type.
expression satisfies Type- Added in TypeScript 4.9
Parameters
| Parameter | Type | Description |
|---|---|---|
$expression | expression | The value being checked. Contextually typed by Type during checking, so object and array literals are still checked against it. |
$Type | type | The constraint the expression must satisfy. Excess property checking applies to object literals just as it would for an annotation. |
Return value
The inferred type of expression, unchangedThe expression's own inferred type, as narrow as it would have been with no annotation at all. The type is only used as a constraint the expression must meet; it never becomes the expression's type.
Errors
A compile-time error when the expression is not assignable to the type - "Type 'X' does not satisfy the expected type 'Y'" - including excess property errors for keys the type does not declare.
Changelog
- PHP 4.9Introduced.
- PHP 5.0const type parameters offer an alternative way to keep literal types narrow when passing values into generic functions.
Type annotations force a choice you should not have to make. Annotate const palette: Record<Colors, string | RGB> and typos in the keys are caught — but every value is now string | RGB, so palette.green.toUpperCase() no longer compiles. Leave the annotation off and the values stay precise, but a misspelled key sails through.
satisfies gives you both. The expression is checked against the type, and then keeps the type it would have inferred on its own.
When to reach for it
Almost every exported constant: route tables, theme objects, config maps, action-to-handler dictionaries. Anywhere you want the compiler to enforce that all cases are covered and no stray keys crept in, while callers still see the exact value at each key.
satisfies Record<Colors, unknown> is the minimal form when you only care that the key set is exactly right.
What trips people up
It is a check, not a cast. There is no runtime effect and no widening — but also no narrowing: it will never turn string into "red" the way as const does. The two are complementary, and {...} as const satisfies Config is a common pairing.
Excess property checking still applies, so an extra key on an object literal is an error even though the value type is preserved.
Example
When to use it
- Verify a config object covers every key of a union while keeping each value's exact type
- Catch a typo in an exported route or theme table at compile time
- Constrain an action handler map without collapsing every handler to one signature
Frequently asked questions
What is the difference between satisfies and a type annotation?
An annotation replaces the expression's type with the annotated one, so all inference detail is lost; satisfies only checks assignability and leaves the inferred type alone. Use an annotation when you want the variable to have that exact type, and satisfies when you want the check without the widening.
Should I use satisfies or as const?
They do different jobs and combine well. as const narrows literals and adds readonly but checks nothing; satisfies checks against a type but never narrows. Write `{...} as const satisfies Config` when you want literal types and validation together.
Does satisfies do anything at runtime?
No. It is erased entirely during compilation, exactly like a type annotation, and emits no check or code. Reach for a runtime validation library if you need the shape verified against data you did not write.
More examples
Why the plain annotation is not enough
These are the three states of the same object. Case 1 keeps green as string but never validates the key set. Case 2 validates the keys and then flattens every value to string | RGB, breaking the call. Case 3 reports the same typo as case 2 while c.green remains a string, which is the whole reason the operator exists.
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
// 1. No annotation: precise, but the typo is invisible
const a = { red: [255, 0, 0], green: "#00ff00", bleu: [0, 0, 255] };
a.green.toUpperCase(); // fine, but "bleu" was never checked
// 2. Annotation: typo caught, precision lost
const b: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255], // error: 'bleu' does not exist in type
};
// b.green.toUpperCase();
// error: Property 'toUpperCase' does not exist on type 'string | RGB'
// 3. satisfies: both
const c = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255], // error: typo still caught
} satisfies Record<Colors, string | RGB>;
c.green.toUpperCase(); // and this still worksChecking only the keys, and pairing with as const
Record<Colors, unknown> is the idiom for exhaustiveness alone - every colour must appear, nothing else may, and each value keeps its own type, so favorites.green is boolean. The routes block shows the combination: without as const the method values would widen to string and fail the Route check is not the issue, but ListMethod would be string rather than the literal "GET" that a discriminated switch needs.
type Colors = "red" | "green" | "blue";
// unknown as the value type: check the key set, ignore the values
const favorites = {
red: "yes",
green: false,
blue: "kinda",
// platypus: false, // error: not a member of Colors
} satisfies Record<Colors, unknown>;
const g: boolean = favorites.green; // still boolean, not unknown
// satisfies validates; as const narrows. Use both when you need both.
type Route = { path: string; method: "GET" | "POST" };
const routes = {
list: { path: "/users", method: "GET" },
create: { path: "/users", method: "POST" },
} as const satisfies Record<string, Route>;
type RouteName = keyof typeof routes; // "list" | "create"
type ListMethod = typeof routes.list.method; // "GET", not string
Discussion