TypeScript's satisfies
operator solves a common dilemma in type checking: how to verify that a value matches a type while keeping its precise type information. Before satisfies
, developers had to choose between type safety and type precision - now we can have both.
What Problem Does It Solve?
When working with TypeScript, we often face a choice:
- Use type annotations (
:type
) - which gives us type checking but can widen our types - Use type assertions (
as const
) - which preserves literal types but skips type checking - Use
satisfies
- which gives us the best of both worlds!
Think of satisfies
as a type checker that validates your code without changing how TypeScript sees your values. It's particularly useful when working with object literals, string literals, and arrays where you want to maintain exact types while ensuring type safety.
Let's look at the type widening problem first:
const colors = {
primary: "#0077ff", // 😯 type widens to string
secondary: "#1a2b3c", // 😯 type widens to string
};
// TypeScript infers this type:
type ColorsType = {
primary: string; // widened!
secondary: string; // widened!
}
// This means we can assign any string, even invalid ones:
colors.primary = "not-a-valid-color"; // TypeScript allows this! 😱
// After - with satisfies
const colors = {
primary: "#0077ff",
secondary: "#1a2b3c"
} satisfies Record<string, `#${string}`>; // ensures hex color format
// Now this would be a type error:
colors.primary = "not-a-valid-color"; // 🚫 Error! Must start with #
This type widening can lead to bugs because TypeScript loses the precise information about our color values. The satisfies
operator helps us maintain this precision while still getting type checking benefits.
Let's look at some practical examples of how satisfies
improves type safety:
Example 1: Object Literals
// Without satisfies - TypeScript forgets our exact colors
const theme = {
primary: "#0077ff",
secondary: "#1a2b3c",
success: "#00ff00"
};
// type is { primary: string, secondary: string, success: string }
// With satisfies - TypeScript remembers our exact colors
const themeWithSatisfies = {
primary: "#0077ff",
secondary: "#1a2b3c",
success: "#00ff00"
} satisfies Record<string, string>;
// ✅ type is { primary: "#0077ff", secondary: "#1a2b3c", success: "#00ff00" }
// why it matters:
// Without satisfies
theme.primary.toUpperCase(); // OK, but loses autocomplete for specific hex value operations
// With satisfies
themeWithSatisfies.primary.slice(1); // Has full autocomplete and type safety for "#0077ff"
Example 2: Arrays and Tuples
Here's where things get interesting:
// A tale of two coordinates...
// Without satisfies - loses tuple precision
const coordinates = [
[10, 20],
[30, 40]
]; // type: number[][]
// With satisfies - maintains tuple precision
const preciseCoordinates = [
[10, 20],
[30, 40]
] satisfies [number, number][];
// This works
coordinates.push([50, 60, 70]); // 😱 Oops, 3D coordinate snuck in!
// This errors (yay!)
preciseCoordinates.push([50, 60, 70]); // 🚫 Error: Expected 2 elements
Example 3: A More Practical Example of a Type-Safe API Client
// First, define our valid routes
type ApiRoutes = "/api/users" | "/api/posts";
// And their corresponding response types
type RouteResponses = {
"/api/users": { name: string; id: number }[];
"/api/posts": { title: string; id: number }[];
};
// A type-safe fetch function
function fetchFromApi<T extends ApiRoutes>(url: T): RouteResponses[T] {
return fetched data
}
// Approach 1: Using type annotation
const api1: { routes: { users: string; posts: string } } = {
routes: {
users: "/api/users",
posts: "/api/posts"
}
};
// Approach 2: Using satisfies
const api2 = {
routes: {
users: "/api/users" as const,
posts: "/api/posts" as const
}
} satisfies { routes: { users: ApiRoutes; posts: ApiRoutes } };
// Let's try to fetch some users...
fetchFromApi(api1.routes.users);
// 🚫 Error: Argument of type 'string' is not assignable to parameter of type 'ApiRoutes'
fetchFromApi(api2.routes.users);
// ✅ Works! TypeScript knows this is exactly "/api/users"
// Return type is { name: string; id: number }[]
// We can even make it more concise:
const api3 = {
routes: {
users: "/api/users",
posts: "/api/posts"
}
} satisfies { routes: Record<string, ApiRoutes> };
Key points:
- With type annotation (api1), we lose the literal types and just get
string
- With
satisfies
andas const
(api2), we preserve the exact string literals - With
satisfies
andRecord
(api3), we get a more concise way to type check our routes
This pattern is commonly used in type-safe API clients where the exact URL string determines the return type of the API call.
Best Practices 📚
The satisfies
operator is like a type-checking superhero that:
- Validates your types ✅
- Preserves literal information ✅
- Makes your code more maintainable ✅
- Catches errors at compile time ✅
Conclusion
When to use each:
- Use direct type annotation (
:Type
) when you don't need to preserve literal types - Use
satisfies
when you want both type validation AND literal type preservation - Use
as
rarely, mainly for type assertions when you know more than TypeScript
Remember: With great type safety comes great maintainability! 🕷️
Top comments (0)