DEV Community

Atlas Whoff
Atlas Whoff

Posted on

TypeScript satisfies operator: the most underrated feature you're probably not using

TypeScript 4.9 shipped a quiet feature that I now use in almost every project: the satisfies operator. If you haven't added it to your workflow yet, this will change how you think about type-safe config objects.

The problem it solves

You have a config object and you want two things simultaneously:

  1. Type checking that it matches a certain shape
  2. TypeScript to infer the exact literal types (not widen them)

These two goals conflict with a plain type annotation.

type Routes = Record<string, { path: string; exact?: boolean }>;

// Option A: annotate the type
const routes: Routes = {
  home: { path: '/', exact: true },
  about: { path: '/about' },
};
// routes.home.path is `string` — TypeScript forgot it's "/"
// routes.home.exact is `boolean | undefined` — TypeScript forgot it's `true`

// Option B: no annotation, use as const
const routes = {
  home: { path: '/', exact: true },
  about: { path: '/about' },
} as const;
// TypeScript knows exact values, BUT we lose validation against Routes shape
// Typos like `phat` instead of `path` go undetected
Enter fullscreen mode Exit fullscreen mode

Both options lose something important. satisfies gives you both.

satisfies in action

const routes = {
  home: { path: '/', exact: true },
  about: { path: '/about' },
  // contact: { phat: '/contact' }  // TypeScript catches this typo!
} satisfies Routes;

// Now routes.home.path is "/" (literal), not string
// And routes.home.exact is true (literal), not boolean | undefined
const homePath = routes.home.path; // type: "/"
Enter fullscreen mode Exit fullscreen mode

The object is validated against Routes at compile time, but TypeScript retains the narrow literal types for downstream use.

Real-world patterns where this shines

1. Theme configuration

type Theme = {
  colors: Record<string, string>;
  spacing: Record<string, number>;
};

const theme = {
  colors: {
    primary: '#6366f1',
    secondary: '#8b5cf6',
    error: '#ef4444',
  },
  spacing: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 32,
  },
} satisfies Theme;

// TypeScript knows theme.colors.primary is "#6366f1", not just string
type PrimaryColor = typeof theme.colors.primary; // "#6366f1"
Enter fullscreen mode Exit fullscreen mode

2. API route registry

type ApiRoute = {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  handler: string;
  auth?: boolean;
};

const apiRoutes = {
  getUser: { method: 'GET', handler: 'users/get', auth: true },
  createUser: { method: 'POST', handler: 'users/create' },
  deleteUser: { method: 'DELETE', handler: 'users/delete', auth: true },
  // badRoute: { method: 'FETCH', handler: 'foo' }  // Error: FETCH not in union
} satisfies Record<string, ApiRoute>;

// Each route method is narrowed to its literal: 'GET', 'POST', etc.
type GetUserMethod = typeof apiRoutes.getUser.method; // "GET"
Enter fullscreen mode Exit fullscreen mode

3. Feature flag registry

type FeatureFlag = {
  enabled: boolean;
  rolloutPercent?: number;
  description: string;
};

const flags = {
  newDashboard: {
    enabled: true,
    rolloutPercent: 25,
    description: 'Redesigned analytics dashboard',
  },
  aiAssistant: {
    enabled: false,
    description: 'Claude-powered code assistant',
  },
} satisfies Record<string, FeatureFlag>;

// flags.newDashboard.enabled is `true` (literal), not `boolean`
Enter fullscreen mode Exit fullscreen mode

Combining satisfies with as const

For deeply nested objects where you want complete literal narrowing:

const config = {
  env: 'production',
  features: {
    ssr: true,
    analytics: true,
    experiments: false,
  },
  limits: {
    maxConnections: 100,
    timeoutMs: 5000,
  },
} as const satisfies AppConfig;
// Both fully narrowed AND validated against AppConfig shape
Enter fullscreen mode Exit fullscreen mode

When to use satisfies vs type annotation vs as const

Goal Use
Just validate shape, widened types OK : TypeAnnotation
Exact literal types, no shape validation as const
Both: validate shape + keep literal types satisfies
Both + deeply immutable as const satisfies

The pattern I use in every Next.js project

In lib/config.ts:

type AppConfig = {
  site: {
    name: string;
    url: string;
    description: string;
  };
  auth: {
    providers: Array<'github' | 'google' | 'email'>;
    sessionMaxAge: number;
  };
  stripe: {
    plans: Record<string, { priceId: string; limits: Record<string, number> }>;
  };
};

export const config = {
  site: {
    name: 'My SaaS',
    url: 'https://mysaas.com',
    description: 'The best SaaS you ever used',
  },
  auth: {
    providers: ['github', 'google'],
    sessionMaxAge: 60 * 60 * 24 * 30,
  },
  stripe: {
    plans: {
      pro: {
        priceId: 'price_xxx',
        limits: { apiCalls: 10000, seats: 5 },
      },
    },
  },
} satisfies AppConfig;

// Consumers get full literal type inference:
type SiteName = typeof config.site.name; // "My SaaS"
Enter fullscreen mode Exit fullscreen mode

This means autocomplete works perfectly, refactoring is safe, and you catch mismatches against your config schema at compile time.


If you're building a TypeScript-first SaaS and want a starter that uses patterns like this throughout — typed config, type-safe env vars, Stripe integration — check out the AI SaaS Starter Kit at whoffagents.com. Ships with Next.js 14, Stripe billing, and auth all pre-wired with proper TypeScript patterns.

Top comments (0)