TypeScript satisfies Operator

Validate types without widening

✅ What is satisfies?

The satisfies operator validates that a value matches a type without changing its inferred type. It ensures type safety while preserving specific literal types, giving you the best of both worlds.


const colors = { red: "#FF0000", blue: [0, 0, 255] } satisfies Record<string, string | number[]>;
colors.red.toUpperCase();  // ✓ Knows it's a string
                                    

Key Concepts

🎯

Type Validation

Ensure values match a type

const config = {
  port: 3000
} satisfies Config;
🔍

Preserve Inference

Keep specific literal types

const status = "active" satisfies string;
// Type: "active" (not string)
⚖️

vs Type Annotation

Better than : Type syntax

// Keeps literal type
const x = "hi" satisfies string;
🛡️

Catch Errors

Validate at compile time

const data = {
  name: "Alice"
} satisfies User; // ✗ Error if invalid

🔹 The Problem: Type Annotation Widening

Type annotations widen specific types:

type Colors = {
  red: string;
  blue: string;
  green: string;
};

// Using type annotation
const colors1: Colors = {
  red: "#FF0000",
  blue: "#0000FF",
  green: "#00FF00"
};

// Type is widened to string
const redHex = colors1.red;  // Type: string (not "#FF0000")
// colors1.red.toUpperCase();  // Works, but lost literal type

// Without annotation
const colors2 = {
  red: "#FF0000",
  blue: "#0000FF",
  green: "#00FF00"
};

const blueHex = colors2.blue;  // Type: "#0000FF" (literal!)
// But no validation that it matches Colors type!

🔹 The Solution: satisfies Operator

Validate type while preserving literals:

type Colors = {
  red: string;
  blue: string;
  green: string;
};

const colors = {
  red: "#FF0000",
  blue: "#0000FF",
  green: "#00FF00"
} satisfies Colors;

// ✓ Validated against Colors type
// ✓ Preserves literal types
const redHex = colors.red;  // Type: "#FF0000"
redHex.toUpperCase();       // ✓ Works

// ✗ Error: missing property
// const invalid = {
//   red: "#FF0000"
// } satisfies Colors;

🔹 satisfies with Union Types

Validate while keeping specific types:

type Config = {
  [key: string]: string | number | boolean;
};

const appConfig = {
  appName: "MyApp",
  port: 3000,
  debug: true,
  version: "1.0.0"
} satisfies Config;

// TypeScript knows exact types
appConfig.appName.toUpperCase();  // ✓ Knows it's string
appConfig.port.toFixed(0);        // ✓ Knows it's number
appConfig.debug ? "yes" : "no";   // ✓ Knows it's boolean

// With type annotation, you'd lose this precision
const config2: Config = {
  appName: "MyApp",
  port: 3000
};
// config2.port.toFixed(0);  // ✗ Error: might be string or boolean

🔹 satisfies with Arrays

Validate array types while preserving tuples:

type RGB = [number, number, number];

// With type annotation - becomes mutable array
const color1: RGB = [255, 0, 0];
color1.push(100);  // ✓ Allowed (but shouldn't be!)

// With satisfies - preserves tuple
const color2 = [255, 0, 0] satisfies RGB;
// color2.push(100);  // ✗ Error: tuple has fixed length

// Access with type safety
const red = color2[0];    // Type: number
const green = color2[1];  // Type: number
const blue = color2[2];   // Type: number

🔹 satisfies with Record Types

Validate object shapes while keeping property types:

type ColorFormat = Record<string, string | number[]>;

const palette = {
  red: "#FF0000",           // string
  blue: [0, 0, 255],        // number[]
  green: "#00FF00",         // string
  yellow: [255, 255, 0]     // number[]
} satisfies ColorFormat;

// TypeScript knows exact types for each property
palette.red.toUpperCase();     // ✓ Knows red is string
palette.blue.map(n => n * 2);  // ✓ Knows blue is number[]

// With type annotation, you'd need type guards
const palette2: ColorFormat = {
  red: "#FF0000",
  blue: [0, 0, 255]
};
// palette2.red.toUpperCase();  // ✗ Error: might be number[]

🔹 Practical Example: Route Configuration

Build type-safe route configs:

type RouteConfig = {
  [key: string]: {
    path: string;
    method: "GET" | "POST" | "PUT" | "DELETE";
    auth?: boolean;
  };
};

const routes = {
  home: {
    path: "/",
    method: "GET"
  },
  createUser: {
    path: "/users",
    method: "POST",
    auth: true
  },
  updateUser: {
    path: "/users/:id",
    method: "PUT",
    auth: true
  },
  deleteUser: {
    path: "/users/:id",
    method: "DELETE",
    auth: true
  }
} satisfies RouteConfig;

// TypeScript knows exact method types
if (routes.createUser.method === "POST") {
  console.log("Creating user");
}

// Autocomplete works perfectly
routes.home.path;           // ✓ "/"
routes.createUser.method;   // ✓ "POST"

🔹 satisfies vs as const

Combine both for maximum type safety:

type Status = "pending" | "approved" | "rejected";

// Just satisfies
const status1 = "pending" satisfies Status;
// Type: "pending" (literal)

// Just as const
const status2 = "pending" as const;
// Type: "pending" (literal, but no validation)

// Both together
const status3 = "pending" as const satisfies Status;
// Type: "pending" (literal + validated)

// Practical example
type Config = {
  readonly [key: string]: string | number;
};

const config = {
  host: "localhost",
  port: 3000
} as const satisfies Config;

// Deeply readonly + validated
// config.port = 8080;  // ✗ Error: readonly

🔹 Error Detection with satisfies

Catch mistakes at compile time:

type User = {
  id: number;
  name: string;
  email: string;
};

// ✗ Error: missing email property
// const user1 = {
//   id: 1,
//   name: "Alice"
// } satisfies User;

// ✗ Error: wrong type for id
// const user2 = {
//   id: "1",
//   name: "Bob",
//   email: "[email protected]"
// } satisfies User;

// ✓ Valid
const user3 = {
  id: 1,
  name: "Charlie",
  email: "[email protected]"
} satisfies User;

// Still has literal types
user3.name;  // Type: "Charlie"

🔹 When to Use satisfies

Best practices for using satisfies:

// ✓ Use satisfies when:
// 1. You want type validation + literal types
const theme = {
  primary: "#007bff",
  secondary: "#6c757d"
} satisfies Record<string, string>;

// 2. Working with configuration objects
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
} satisfies AppConfig;

// 3. Defining constants with specific shapes
const HTTP_STATUS = {
  OK: 200,
  NOT_FOUND: 404,
  SERVER_ERROR: 500
} satisfies Record<string, number>;

// ✗ Don't use satisfies when:
// 1. You actually want type widening
const data: string[] = ["a", "b"];  // Want string[], not ["a", "b"]

// 2. Simple type assertions are enough
const value = getValue() as string;

💡 Key Takeaways

  • satisfies validates types without widening them
  • Preserves literal types while ensuring type safety
  • Better than type annotations when you need specific types
  • Combine with as const for deep immutability
  • Perfect for configuration objects and constants
  • Catches type errors at compile time

🧠 Test Your Knowledge

What's the main benefit of satisfies?