The TypeScript satisfies Operator
I had a theme configuration object with twelve color tokens. TypeScript annotated it as ThemeColors, which validated the shape but turned every hex string into plain string — autocomplete for specific color names disappeared. Casting with as const gave me literal types but didn't catch the typo accemt: "#ff0000" where accent was expected. TypeScript 4.9's satisfies operator does both: validates against ThemeColors and keeps each value's literal type. It's the tool for the exact problem that neither annotations nor assertions solve cleanly.
The widening problem
type Color = "red" | "green" | "blue";
const annotation: Record<string, Color> = {
primary: "red",
secondary: "green",
};
// annotation.primary is Color (not "red")
type Primary = typeof annotation.primary; // Color
The annotation validates values but widens literals. You lose the specific type information that powers precise autocomplete and exhaustive checks.
const assertion = {
primary: "red",
secondary: "green",
} as const;
// Validates nothing — this compiles:
const bad = {
primary: "red",
secondary: "green",
accent: "yellow", // not in Color — no error
} as const;
as const preserves literals but doesn't validate against a target type.
satisfies: both checks
type Color = "red" | "green" | "blue";
const palette = {
primary: "red",
secondary: "green",
} satisfies Record<string, Color>;
// palette.primary is "red" (literal preserved)
type Primary = typeof palette.primary; // "red"
// Typos are caught:
const broken = {
primary: "red",
secondary: "yello", // Error: "yello" is not assignable to Color
} satisfies Record<string, Color>;
Validation and literal preservation in one expression.
Configuration objects
The most common use case — app config with known keys and literal values:
interface ServerConfig {
host: string;
port: number;
env: "development" | "staging" | "production";
features: Record<string, boolean>;
}
const config = {
host: "0.0.0.0",
port: 3000,
env: "production",
features: {
darkMode: true,
betaApi: false,
},
} satisfies ServerConfig;
// config.env is "production" (not string)
// config.features.darkMode is true (not boolean)
Downstream code that branches on config.env gets exhaustiveness checking. Downstream code that reads config.features.darkMode knows it's true, not boolean.
Route and API definitions
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type RouteHandler = {
method: HttpMethod;
path: string;
handler: (req: Request) => Response;
};
const routes = {
getUser: {
method: "GET",
path: "/users/:id",
handler: getUserHandler,
},
createUser: {
method: "POST",
path: "/users",
handler: createUserHandler,
},
} satisfies Record<string, RouteHandler>;
// routes.getUser.method is "GET" (literal)
// misspelled method "GETT" would be caught at compile time
CSS and design tokens
type Spacing = `${number}px` | `${number}rem`;
const spacing = {
xs: "4px",
sm: "8px",
md: "16px",
lg: "24px",
xl: "32px",
} satisfies Record<string, Spacing>;
// spacing.md is "16px" — usable in template literal contexts
const style = `padding: ${spacing.md};`;
Event maps and discriminated unions
type EventPayloads = {
click: { x: number; y: number };
keypress: { key: string; shift: boolean };
scroll: { top: number };
};
const handlers = {
click: (payload) => console.log(payload.x, payload.y),
keypress: (payload) => console.log(payload.key),
scroll: (payload) => console.log(payload.top),
} satisfies {
[K in keyof EventPayloads]: (payload: EventPayloads[K]) => void;
};
Each handler's payload is precisely typed for its event. Add a new event to EventPayloads and the compiler requires a matching handler.
satisfies vs. other approaches
| Approach | Validates shape | Preserves literals | Catches extra keys |
|---|---|---|---|
| Type annotation | Yes | No | Depends on type |
as const |
No | Yes | No |
as Type assertion |
No | No | No |
satisfies Type |
Yes | Yes | Depends on type |
Combining with generics
satisfies works in generic contexts for validating computed objects:
function createLookup<T extends Record<string, string>>(entries: T) {
return entries satisfies Record<string, string>;
}
const colors = createLookup({
red: "#ff0000",
green: "#00ff00",
blue: "#0000ff",
});
// colors.red is "#ff0000", not string
When to reach for it
Use satisfies whenever you have a concrete object that should match a type but where you also need the narrowest possible inferred types for its values. Configuration, theme tokens, route tables, translation maps, and enum-like objects are the sweet spots. For function parameters that are already typed by the signature, you don't need it — the parameter type provides the validation.
satisfies vs as const
const config = {
apiUrl: "https://api.example.com",
retries: 3,
} satisfies AppConfig;
// config.retries is number, not literal 3 — usable where number expected
satisfies validates shape without widening to union of literals — better than as Config which loses type checking.
satisfies vs as const vs type annotation
satisfies validates shape without widening — const config = { ... } satisfies Config keeps literal types for keys while checking Config compliance. Type annotation const config: Config = { ... } widens literals to Config's field types. as const freezes values but does not validate against an interface. Use satisfies when you want both inference and validation — config objects, theme tokens, route maps.
satisfies with discriminated unions
Theme config objects use satisfies Record<string, ThemeToken> while preserving literal keys for autocomplete. Combined with as const on nested values, designers get typed token names without losing string literal inference for CSS variable generation.
Resources
- TypeScript 4.9 Release Notes: satisfies
- TypeScript Playground: satisfies examples
- Total TypeScript: satisfies
- TypeScript Handbook: Literal Types
- TypeScript Handbook: Type Assertions
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
typescript satisfies operator rollout
Field RUM on Android 4G. RDS Proxy where relevant. Rollback in PR.
Trade-offs I keep revisiting for typescript satisfies operator
TypeScript leverage for typescript satisfies operator comes from encoding invariants the compiler can enforce at change sites. any escapes and loose as casts are where production bugs hide.
For typescript satisfies operator:
- Prefer
unknown+ narrowing overany - Branded types for IDs that must not mix (UserId vs OrderId)
- Zod (or equivalent) at IO boundaries; infer types from schemas
satisfiesfor config objects that need both literal inference and type checks
Enable strictness incrementally with lint gates so new code cannot regress the baseline.
| Signal | Target | Alarm |
|---|---|---|
| Coverage % | Team-defined SLO | Page on burn rate |
| Mean time to detect | Baseline − noise | Ticket if sustained |
| Escapes to prod | Budget cap | Weekly review |
Metrics and alarms for typescript satisfies operator
Reviewers should challenge assumptions encoded in typescript satisfies operator: defaults copied from tutorials, timeouts that exceed upstream SLAs, and authz checks applied only on the primary UI path. Require a short threat or failure note in the PR when the change touches a trust boundary.
Concrete probes:
- Scenario B for typescript satisfies operator: bad config shipped — prove rollback within the declared RTO without data corruption.
- Scenario C for typescript satisfies operator: traffic 3× baseline — prove autoscaling or shedding keeps the golden journey healthy.
- Scenario A for typescript satisfies operator: partial dependency outage — prove clients degrade gracefully and retries do not amplify load.
Rollout sequence that worked for typescript satisfies operator
Roll out typescript satisfies operator behind a flag or weighted route when possible. Start with internal users or a low-risk geography. Watch the signals in the table for at least one full business cycle before calling the migration done. Keep the previous path warm until error budgets stabilize.
Document the owner, the dashboard, and the single command that reverts the change. If that sentence is hard to write, the design is not ready for production traffic.
Multi-tenant concerns in typescript satisfies operator
Detail 1 (357): for typescript satisfies operator, define the contract between producers and consumers explicitly — payload shape, timeout, and idempotency key. When multi-tenant concerns in typescript satisfies operator becomes painful, it is usually because that contract was implicit.
I keep a short matrix: who can break typescript satisfies operator, how we detect it within five minutes, and who is paged. Update the matrix when ownership moves. Add one synthetic check that exercises the failure path, not only the happy path. Prefer checks that run continuously over quarterly manual reviews that everyone skips under deadline pressure.
If you only remember one thing about typescript satisfies operator: optimize for reversible decisions. Reversibility beats cleverness when the incident channel is busy and the blast radius is unclear.
Compliance evidence for typescript satisfies operator
Detail 2 (577): for typescript satisfies operator, define the contract between producers and consumers explicitly — payload shape, timeout, and idempotency key. When compliance evidence for typescript satisfies operator becomes painful, it is usually because that contract was implicit.
I keep a short matrix: who can break typescript satisfies operator, how we detect it within five minutes, and who is paged. Update the matrix when ownership moves. Add one synthetic check that exercises the failure path, not only the happy path. Prefer checks that run continuously over quarterly manual reviews that everyone skips under deadline pressure.
If you only remember one thing about typescript satisfies operator: optimize for reversible decisions. Reversibility beats cleverness when the incident channel is busy and the blast radius is unclear.
Frequently asked questions
What does the satisfies operator do in TypeScript?
The satisfies operator checks that an expression conforms to a type without changing the expression's inferred type. Unlike a type annotation, which widens literal types to their base type, satisfies preserves narrow literal types while still validating the shape. You get both compile-time checking and precise autocomplete for literal values like color names, route paths, or configuration keys.
How is satisfies different from a type annotation?
A type annotation like const config: Config = { ... } validates the shape but widens inferred types — a literal 'red' becomes string. The satisfies operator const config = { ... } satisfies Config validates the same shape but keeps 'red' as the literal type 'red'. This matters when you want exhaustiveness checking plus precise downstream inference for individual property values.
When should I use satisfies instead of as const?
Use as const when you want everything deeply readonly and literal with no target type to validate against. Use satisfies when you need to check against a specific type — like a Record of route handlers or a theme config interface — while preserving literal types. satisfies gives you validation that as const cannot provide, because as const doesn't check whether your object actually matches an expected shape.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →