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 constfor deep immutability - Perfect for configuration objects and constants
- Catches type errors at compile time