DEV Community

Cover image for TypeScript 'Satisfies' Operator: Complete Guide with Examples
Helen Small
Helen Small

Posted on • Originally published at devunus.com

TypeScript 'Satisfies' Operator: Complete Guide with Examples

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:

  1. Use type annotations (:type) - which gives us type checking but can widen our types
  2. Use type assertions (as const) - which preserves literal types but skips type checking
  3. 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 #
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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> };
Enter fullscreen mode Exit fullscreen mode

Key points:

  1. With type annotation (api1), we lose the literal types and just get string
  2. With satisfies and as const (api2), we preserve the exact string literals
  3. With satisfies and Record (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)