The satisfies operator
One of the most frustrating moments in TypeScript development is losing precise type information the moment you assign a value to a variable. You want to validate that an object conforms to a specific shape, but you also want to preserve the exact literal types of its properties. That's where the satisfies operator comes in—it lets you check a value against a type without losing its literal types in the process.
Why you need satisfies
Before TypeScript 4.9 introduced the satisfies operator, developers faced a classic trade-off. You could either use a type annotation to enforce a contract, which would widen your literal types, or you could skip validation entirely and risk runtime mismatches.
Here's the problem in action. Imagine you're building a routing configuration object:
const routes: Record<string, string> = {
home: '/',
blog: '/blog',
about: '/about',
};
// routes.home has type `string`, not `'/'`
// You've lost the literal type information
When you annotate with Record<string, string>, TypeScript widens every property to the generic type string. That means later when you reference routes.home, TypeScript sees it as just string, not the literal '/'. This becomes a real problem when you pass route values to functions that expect specific paths or when you want autocomplete to show you the exact values available.
The satisfies operator solves this by validating the shape without widening the types.
Validates the shape
The primary job of satisfies is to ensure your value matches the structure you expect. It acts as a compile-time contract checker that never changes the inferred type of your value.
Consider a configuration object for a payment processor:
type PaymentConfig = {
apiKey: string;
timeout: number;
retryCount: number;
};
const config = {
apiKey: 'sk_live_abc123',
timeout: 5000,
retryCount: 3,
} satisfies PaymentConfig;
// All properties exist and have correct types
// If you remove apiKey or use wrong type, TypeScript errors
If you forget a required property or provide the wrong type, TypeScript catches it immediately. This validation happens at the type level, so there's zero runtime overhead. The satisfies operator is purely a development-time feature.
You can also use satisfies with more complex type structures. For example, validating that a discriminated union is constructed correctly:
type Message =
| { type: 'success'; code: 200; message: string }
| { type: 'error'; code: 400 | 500; message: string };
const response = {
type: 'error',
code: 500,
message: 'Internal server error',
} satisfies Message;
// TypeScript ensures the type/code combination is valid
Keeps precise inferred types
Here's where satisfies truly shines. Unlike type annotations, satisfies preserves the literal types of your properties. This means TypeScript still knows the exact value, not just its general category.
const routes = {
home: '/',
blog: '/blog',
admin: '/admin',
} satisfies Record<string, string>;
// routes.home has type '/' (literal)
// Not just 'string'
// routes.blog has type '/blog' (literal)
type BlogPath = typeof routes.blog; // '/blog'
This precision is invaluable when you need to work with those values later. Imagine a function that only accepts specific route paths:
function navigate(path: '/' | '/blog' | '/admin') {
window.location.href = path;
}
navigate(routes.home); // OK - '/' is valid
navigate(routes.blog); // OK - '/blog' is valid
navigate(routes.admin); // OK - '/admin' is valid
// Without satisfies, you'd need type assertions or
// TypeScript would reject these calls
The key difference is that satisfies validates without transforming. The type of each property stays exactly as TypeScript inferred it from the value itself.
Great for config objects
Configuration objects are where satisfies becomes genuinely practical. Most apps have multiple config files or objects that need to be validated but also need their literal values preserved for type-safe feature flags, constants, or route definitions.
Here's a real-world scenario from an e-commerce platform I built:
type EnvironmentConfig = {
apiUrl: string;
environment: 'development' | 'staging' | 'production';
features: Record<string, boolean>;
timeouts: Record<string, number>;
};
const appConfig = {
apiUrl: 'https://api.example.com',
environment: 'production',
features: {
darkMode: true,
betaCheckout: false,
analyticsV2: true,
},
timeouts: {
api: 5000,
cache: 60000,
retry: 3000,
},
} satisfies EnvironmentConfig;
// Now you can use the literal values
function isFeatureEnabled(feature: keyof typeof appConfig.features) {
return appConfig.features[feature];
}
// And TypeScript knows appConfig.environment is literally 'production'
if (appConfig.environment === 'production') {
// This works because TypeScript narrows the type correctly
}
Without satisfies, you'd either lose the literal types entirely or have to maintain the configuration twice—once for runtime and once for types. satisfies lets you have both.
Better than type assertions
Before satisfies, developers often used type assertions (the as keyword) to force TypeScript to accept a value as a particular type. This is a dangerous pattern because it completely bypasses type checking.
// Old approach - NOT RECOMMENDED
const config = {
apiKey: 'sk_live_abc123',
timeout: 5000,
// Oops, forgot retryCount
} as PaymentConfig;
// TypeScript doesn't complain, but this is broken at runtime
The as assertion doesn't validate anything. It just tells TypeScript "trust me, I know this is the right type." If you made a mistake, TypeScript won't catch it.
satisfies validates without asserting:
const config = {
apiKey: 'sk_live_abc123',
timeout: 5000,
// Missing retryCount - TypeScript errors here!
} satisfies PaymentConfig;
// Error: Property 'retryCount' is missing
satisfies also preserves literal types where assertions would widen them:
const status = 'active' as string; // type is 'string'
const status2 = 'active' satisfies string; // type is 'active'
This makes satisfies the superior choice for most real-world validation scenarios. It catches mistakes while keeping your types precise.
Common mistakes
Mistake 1: Using satisfies as a type annotation
satisfies is not a replacement for type annotations. You still need explicit types for function parameters, class properties, and exported values that other code depends on.
// This only validates within this file
const config = { /* ... */ } satisfies Config;
// For a public API, still use explicit types
export const publicConfig: Config = { /* ... */ };
Mistake 2: Over-constraining with satisfies
If your type is too strict, satisfies becomes restrictive rather than permissive. Remember that satisfies checks shape—if your type requires specific literal values, you need to provide them exactly.
type Config = {
mode: 'light' | 'dark';
};
const config = {
mode: 'LIGHT', // Error - doesn't satisfy the type
} satisfies Config;
Mistake 3: Forgetting that satisfies doesn't work at runtime
satisfies is a compile-time feature. If you receive data from an API or user input, you still need runtime validation with a library like Zod or io-ts.
// User input - could be anything
const userConfig = JSON.parse(configJson);
// This won't protect you at runtime
const validated = userConfig satisfies Config; // Still vulnerable!
// Use runtime validation instead
const validated = configSchema.parse(userConfig);
Key takeaways
- satisfies validates that a value matches a type without widening its inferred types to generic forms.
- Use satisfies for local configuration objects, constants, and any value where you need both validation and precise literal types.
- satisfies is strictly better than type assertions because it validates without bypassing type safety.
- preserves literal type information, allowing autocomplete and type narrowing to work correctly.
- Remember that satisfies is compile-time only—it doesn't replace runtime validation for untrusted data sources.
- For public APIs and exported values, pair satisfies with explicit type annotations for clarity.






Comments
Loading comments…