Skip to main content
Ganesh Joshi
Back to Blogs

TypeScript satisfies: safer types without losing inference

February 15, 20265 min read
Tutorials
TypeScript code on screen

TypeScript's satisfies operator (introduced in 4.9) provides type validation without type widening. You can ensure a value matches a type while keeping the most specific type possible. This solves a common tension between safety and precise inference.

The problem

Consider this configuration object:

type Config = {
  theme: 'light' | 'dark';
  language: string;
};

// With type annotation
const config: Config = {
  theme: 'dark',
  language: 'en',
};

// config.theme is 'light' | 'dark', not 'dark'
// We lost the literal type!

The annotation widens config.theme from 'dark' to 'light' | 'dark'. Sometimes you need the literal.

Enter satisfies

const config = {
  theme: 'dark',
  language: 'en',
} satisfies Config;

// config.theme is 'dark' (literal preserved!)
// TypeScript still validates against Config

You get validation AND precise inference.

How satisfies differs from annotation

Approach Validation Resulting type
const x: Type = value Yes Type (widened)
const x = value satisfies Type Yes Inferred (narrow)
const x = value as Type No Type (trusted)
type Theme = 'light' | 'dark';

// Annotation: widened to Theme
const theme1: Theme = 'dark';
// theme1: Theme

// Satisfies: stays literal
const theme2 = 'dark' satisfies Theme;
// theme2: 'dark'

// Assertion: no validation!
const theme3 = 'oops' as Theme;
// No error, theme3: Theme

Use case: configuration objects

type ButtonVariants = {
  [key: string]: {
    background: string;
    color: string;
    border?: string;
  };
};

const variants = {
  primary: {
    background: 'blue',
    color: 'white',
  },
  secondary: {
    background: 'gray',
    color: 'black',
    border: '1px solid black',
  },
} satisfies ButtonVariants;

// variants.primary is precisely typed
// variants.primary.background is 'blue', not string

You validate the shape while keeping specific property types.

Use case: route definitions

type Route = {
  path: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
};

const routes = {
  getUser: { path: '/users/:id', method: 'GET' },
  createUser: { path: '/users', method: 'POST' },
  updateUser: { path: '/users/:id', method: 'PUT' },
} satisfies Record<string, Route>;

// routes.getUser.method is 'GET', not 'GET' | 'POST' | ...
// Autocomplete works on route keys

Use case: theme definitions

type ColorScale = Record<number, string>;

const colors = {
  gray: {
    100: '#f7fafc',
    200: '#edf2f7',
    300: '#e2e8f0',
    400: '#cbd5e0',
    500: '#a0aec0',
  },
  blue: {
    100: '#ebf8ff',
    200: '#bee3f8',
    300: '#90cdf4',
  },
} satisfies Record<string, ColorScale>;

// TypeScript knows colors.gray exists
// colors.gray[100] is '#f7fafc'
// colors.gray[600] would error - doesn't exist!

Use case: enum-like objects

type Status = 'pending' | 'active' | 'completed';

const STATUS = {
  PENDING: 'pending',
  ACTIVE: 'active',
  COMPLETED: 'completed',
} satisfies Record<string, Status>;

// STATUS.PENDING is 'pending', not Status
// Useful for switch exhaustiveness

Satisfies with arrays

type MenuItem = {
  label: string;
  href: string;
};

const menu = [
  { label: 'Home', href: '/' },
  { label: 'About', href: '/about' },
  { label: 'Contact', href: '/contact' },
] satisfies MenuItem[];

// menu[0].label is 'Home'
// But still validated as MenuItem[]

Combining with as const

For fully immutable literals:

const config = {
  api: 'https://api.example.com',
  timeout: 5000,
} as const satisfies {
  api: string;
  timeout: number;
};

// config.api is 'https://api.example.com' (readonly)
// config.timeout is 5000 (readonly)
// And validated against the type

Error detection

satisfies catches errors that as would miss:

type Config = {
  debug: boolean;
  level: 'info' | 'warn' | 'error';
};

// satisfies catches the error
const config = {
  debug: true,
  level: 'verbose', // Error: 'verbose' not assignable to 'info' | 'warn' | 'error'
} satisfies Config;

// as would hide it
const config2 = {
  debug: true,
  level: 'verbose', // No error! Unsafe
} as Config;

When to use which

Scenario Use
Function parameters Type annotation
Return types Type annotation
Variables with specific type satisfies
Config objects satisfies
Type narrowing impossible as (carefully)
Generic constraints Type annotation

Practical examples

API response types

type ApiResponse<T> = {
  data: T;
  status: 'success' | 'error';
};

const response = {
  data: { id: 1, name: 'Alice' },
  status: 'success',
} satisfies ApiResponse<{ id: number; name: string }>;

// response.status is 'success', not 'success' | 'error'

Form defaults

type FormValues = {
  name: string;
  email: string;
  subscribe: boolean;
};

const defaults = {
  name: '',
  email: '',
  subscribe: false,
} satisfies FormValues;

// Validated shape, precise types
// defaults.subscribe is false (boolean literal)

Event handlers map

type EventHandler = (event: Event) => void;

const handlers = {
  click: (e) => console.log('clicked', e),
  scroll: (e) => console.log('scrolled', e),
  resize: (e) => console.log('resized', e),
} satisfies Record<string, EventHandler>;

// handlers.click is validated
// Keys are autocomplete-able

Limitations

satisfies doesn't:

  • Create a new type (just validates)
  • Work with generics in all contexts
  • Replace proper type annotations for APIs
// Still need annotation for function params
function process(config: Config) { ... }

// satisfies is for values, not types
type MyConfig = ... satisfies SomeType; // Error

Summary

The satisfies operator validates that values match types without widening them. Use it for configuration objects, route maps, theme definitions, and anywhere you need both type safety and literal inference. It catches errors that as assertions would miss while preserving the precise types that annotations would lose.

Frequently Asked Questions

The satisfies operator validates that an expression matches a type without widening it. You get type checking while preserving narrow literal types and better autocomplete.

With annotation (const x: Type = value), x has type Type. With satisfies (const x = value satisfies Type), x has the narrower inferred type. Satisfies validates without widening.

Use satisfies for validation—it catches errors at compile time. Use as for assertions when you know better than TypeScript. Satisfies is safer; as can hide mistakes.

Yes. It's ideal for config objects and maps where you want to validate the shape while keeping literal types for keys and values. This gives you autocomplete and type safety.

Yes. Array elements are validated against the type while preserving tuple types and literal element types when possible. It's useful for defining constant arrays.

Related Posts