TypeScript satisfies - Type Safety Without Losing Inference

Validate types without widening them. Keep autocomplete. Catch errors. No type casts needed.

TypeScript 4.9 added satisfies - a way to check that a value matches a type without forcing the value to be that type.

The Problem

You want to validate that an object matches a type, but you also want to keep the specific literal types for autocomplete and type narrowing.

❌ Without satisfies

type Colors = "red" | "green" | "blue";

const palette: Record<string, Colors> = {
primary: "red",
secondary: "green"
};

// ❌ Type is widened to Colors
palette.primary; // type: Colors
// No autocomplete for specific color
// Can't use string methods safely

✅ With satisfies

type Colors = "red" | "green" | "blue";

const palette = {
primary: "red",
secondary: "green"
} satisfies Record<string, Colors>;

// ✅ Type is narrowed to literal
palette.primary; // type: "red"
// Full autocomplete
// Safe to use string methods

What is satisfies?

satisfies is a TypeScript operator that validates a value against a type without changing the inferred type of that value.

Syntax:

const value = expression satisfies Type;

TypeScript checks that expression is assignable to Type, but the type of value remains the narrowest possible type inferred from expression.


The problem it solves

Before satisfies, you had two bad options:

Option 1: No type annotation

const colors = {
  primary: "red",
  secondary: "green",
};

Good: Full autocomplete. colors.primary has type "red".

Bad: No validation. If you typo a color, TypeScript does not catch it.

const colors = {
  primary: "redd", // Typo! But TypeScript is fine with it.
  secondary: "green",
};

Option 2: Type annotation

type Colors = "red" | "green" | "blue";

const colors: Record<string, Colors> = {
  primary: "red",
  secondary: "green",
};

Good: Validation. TypeScript catches typos.

Bad: Type is widened. colors.primary has type Colors, not "red". You lose literal types and autocomplete.

satisfies gives you both:

const colors = {
  primary: "red",
  secondary: "green",
} satisfies Record<string, Colors>;

Good: Validation. TypeScript catches typos.

Good: colors.primary has type "red". Autocomplete works.


Real-world examples

Config objects

You have a config with environment-specific values. You want to ensure all required keys are present, but keep the specific values typed.

Without satisfies:

type Config = {
  apiUrl: string;
  timeout: number;
};

const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

// ❌ Type is widened to string
config.apiUrl; // type: string
// Can't check if it's the production or dev URL

With satisfies:

const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
} satisfies Config;

// ✅ Type is literal
config.apiUrl; // type: "https://api.example.com"
// Full control over exact values

Route definitions

You define routes with specific paths and methods. You want type safety, but also want to reference the exact path strings later.

Without satisfies:

type Route = {
  path: string;
  method: "GET" | "POST";
};

const routes: Route[] = [
  { path: "/users", method: "GET" },
  { path: "/users", method: "POST" },
];

// ❌ Type is widened
routes[0].path; // type: string
// Can't use the exact path in other code

With satisfies:

const routes = [
  { path: "/users", method: "GET" },
  { path: "/users", method: "POST" },
] satisfies Route[];

// ✅ Type is narrowed
routes[0].path; // type: "/users"
routes[0].method; // type: "GET"
// Full literal types preserved

Theme configuration

You have a design system with color tokens. You want to validate the structure, but keep specific color values for autocomplete.

Without satisfies:

type Theme = Record<string, string | number>;

const theme: Theme = {
  primaryColor: "#3b82f6",
  fontSize: 16,
};

// ❌ Type is widened
theme.primaryColor; // type: string | number
// Lost specific color value

With satisfies:

const theme = {
  primaryColor: "#3b82f6",
  fontSize: 16,
} satisfies Theme;

// ✅ Type is narrowed
theme.primaryColor; // type: "#3b82f6"
theme.fontSize; // type: 16
// Autocomplete shows exact values

API response shapes

You fetch data from an API. You want to ensure the shape is correct, but keep the specific values typed for later use.

Without satisfies:

type User = {
  id: number;
  name: string;
  role: "admin" | "user";
};

const mockUser: User = {
  id: 1,
  name: "Alice",
  role: "admin",
};

// ❌ Type is widened
mockUser.role; // type: "admin" | "user"
// Can't narrow based on the specific value

With satisfies:

const mockUser = {
  id: 1,
  name: "Alice",
  role: "admin",
} satisfies User;

// ✅ Type is narrowed
mockUser.role; // type: "admin"
// Exact literal type preserved

When NOT to use satisfies

satisfies is great for validation + inference, but not everything.

Skip it when:

  • You actually want type widening - if you want string instead of "red", use a type annotation.
  • The inferred type is good enough - if you don't need validation, just let TypeScript infer.
  • You need runtime validation - satisfies is compile-time only. Use Zod, io-ts, or similar for runtime checks.
  • TypeScript < 4.9 - satisfies was added in TypeScript 4.9 (November 2022). If you are on an older version, upgrade or use type annotations.

Use it when:

  • You want to validate structure without losing literal types
  • You need autocomplete for specific values
  • You are building config objects, route tables, or design tokens
  • You want both type safety and precise inference

Combining with other features

With const assertions

You can combine satisfies with as const for maximum precision:

const routes = [
  { path: "/users", method: "GET" },
  { path: "/users", method: "POST" },
] as const satisfies readonly Route[];

Now:

  • routes is readonly (immutable)
  • Each path and method is a literal type
  • TypeScript validates the structure

This is useful for data that should never change at runtime.


With generics

satisfies works with generic types:

type Response<T> = {
  data: T;
  status: number;
};

const userResponse = {
  data: { id: 1, name: "Alice" },
  status: 200,
} satisfies Response<{ id: number; name: string }>;

// ✅ Full inference
userResponse.data.id; // type: 1
userResponse.data.name; // type: "Alice"

With mapped types

You can use satisfies to validate mapped types while keeping literal keys:

type Handlers = Record<string, () => void>;

const handlers = {
  onClick: () => console.log("clicked"),
  onHover: () => console.log("hovered"),
} satisfies Handlers;

// ✅ Literal keys preserved
Object.keys(handlers); // ("onClick" | "onHover")[]

Browser / runtime support

satisfies is a TypeScript-only feature. It compiles away to nothing in JavaScript.

// TypeScript
const config = {
  apiUrl: "https://api.example.com",
} satisfies Config;

// Compiled JavaScript
const config = {
  apiUrl: "https://api.example.com",
};

No runtime overhead. No polyfills needed. Just type checking at compile time.


Common patterns

Config validation

type AppConfig = {
  api: { url: string; timeout: number };
  features: { analytics: boolean; darkMode: boolean };
};

export const config = {
  api: {
    url: "https://api.example.com",
    timeout: 5000,
  },
  features: {
    analytics: true,
    darkMode: true,
  },
} satisfies AppConfig;

Now other files can import config and get full autocomplete + type safety.


Route tables

type Route = {
  path: string;
  component: React.ComponentType;
};

export const routes = [
  { path: "/", component: HomePage },
  { path: "/about", component: AboutPage },
] satisfies Route[];

Each route is validated, but you still get literal types for paths.


Design tokens

type ColorToken = string;
type SizeToken = number;

export const tokens = {
  colors: {
    primary: "#3b82f6",
    secondary: "#10b981",
  },
  sizes: {
    sm: 12,
    md: 16,
    lg: 20,
  },
} satisfies {
  colors: Record<string, ColorToken>;
  sizes: Record<string, SizeToken>;
};

// ✅ Autocomplete for exact values
tokens.colors.primary; // type: "#3b82f6"

Try it yourself

Open your TypeScript project and try this:

type Config = {
  apiUrl: string;
  timeout: number;
};

// Without satisfies
const config1: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

config1.apiUrl; // Hover: type is string

// With satisfies
const config2 = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
} satisfies Config;

config2.apiUrl; // Hover: type is "https://api.example.com"

The difference is immediate. Autocomplete is better. Types are more precise.


Resources

Copy. Paste. Type check. Your types are now precise and safe.