We've all written `as SomeType` and felt slightly dirty about it. It's the TypeScript equivalent of a `// @ts-ignore` with extra steps — you're telling the compiler to trust you, and the compiler, being a compiler, has no choice but to comply. The `satisfies` keyword, introduced in TypeScript 4.9, is a fundamentally different tool that most people either don't know about or confuse with type assertions. Let's fix that.
The Problem with 'as'
Type assertions (`as`) silence the compiler. They don't validate — they override. When you write `const config = someValue as Config`, TypeScript stops asking questions. It trusts you. And if you're wrong, you get a runtime error at 2am, not a compile error at development time where it costs nothing to fix.
type Theme = {
colors: {
primary: string;
secondary: string;
};
};
// This compiles fine. TypeScript shuts up.
// But you've thrown away the specific type information.
const theme = {
colors: {
primary: '#3b82f6',
secondary: '#8b5cf6',
// oops, typo in the next line won't be caught
primar: '#ff0000',
},
} as Theme;
// TypeScript thinks theme.colors.primary is 'string'
// not the literal '#3b82f6' — you've lost that precision
console.log(theme.colors.primary); // string, not '#3b82f6'Notice two problems: the typo `primar` goes uncaught, and you've lost the literal type. TypeScript no longer knows the exact value — just that it's *a* string. That matters more than you'd think when you're autocompleting, refactoring, or using the values to derive other types.
Enter 'satisfies'
`satisfies` validates that a value matches a type at the point of assignment, but it preserves the most specific type TypeScript can infer. Think of it as 'prove this fits the shape, but keep the details'. The value still gets its inferred type, not the broader declared type.
type Theme = {
colors: {
primary: string;
secondary: string;
};
};
const theme = {
colors: {
primary: '#3b82f6',
secondary: '#8b5cf6',
// Error: Object literal may only specify known properties
// primar: '#ff0000',
},
} satisfies Theme;
// TypeScript STILL knows the literal value
// theme.colors.primary is '#3b82f6', not just 'string'
type PrimaryColor = typeof theme.colors.primary;
// PrimaryColor = '#3b82f6'
// Autocomplete works on the literal values too
console.log(theme.colors.primary.toUpperCase()); // fine, it's a string methodThe typo gets caught. The literal types are preserved. You get the best of both worlds: type safety during declaration AND precise inference downstream. This is not a small thing.
Real-world Cases Where satisfies Wins
The abstract explanation is nice but let's talk about where this actually shows up in real code. We use this pattern constantly in config objects, route definitions, and anything where you want to constrain a structure but still need to derive types from it later.
Route configs are a perfect example. You want to make sure every route has the right shape, but you also want TypeScript to know the exact route keys so you can use them as union types elsewhere:
type RouteConfig = {
path: string;
protected: boolean;
label: string;
};
const routes = {
home: { path: '/', protected: false, label: 'Home' },
dashboard: { path: '/dashboard', protected: true, label: 'Dashboard' },
settings: { path: '/settings', protected: true, label: 'Settings' },
} satisfies Record<string, RouteConfig>;
// This works — TypeScript knows the exact keys
type AppRoute = keyof typeof routes;
// AppRoute = 'home' | 'dashboard' | 'settings'
// This would NOT work if you'd used:
// const routes: Record<string, RouteConfig> = { ... }
// because keyof would just give you 'string'
function navigate(route: AppRoute) {
window.location.href = routes[route].path;
}With a plain type annotation (`const routes: Record<string, RouteConfig>`), you'd get `keyof typeof routes = string`. Useless. With `satisfies`, you get the literal union. That's the whole game.
The Pattern That Bites Everyone: Mixed-Type Objects
Here's a subtler case that catches people off guard. Imagine you have a config where values can be either a string or an array of strings. TypeScript infers the specific type per key, which is exactly what you want:
type AllowedValues = string | string[];
const permissions = {
read: 'public',
write: ['admin', 'editor'],
delete: ['admin'],
} satisfies Record<string, AllowedValues>;
// TypeScript knows these specific types:
// permissions.read is 'public' (a string)
// permissions.write is string[]
// permissions.delete is string[]
// So you can call array methods on write without casting:
permissions.write.includes('admin'); // ✅ fine
// And string methods on read:
permissions.read.toUpperCase(); // ✅ fine
// Compare to what you'd get with a type annotation:
const permissionsAnnotated: Record<string, AllowedValues> = {
read: 'public',
write: ['admin', 'editor'],
delete: ['admin'],
};
// Now every value is 'string | string[]'
// This fails — TypeScript doesn't know read is specifically a string:
permissionsAnnotated.read.toUpperCase();
// Error: Property 'toUpperCase' does not exist on type 'string | string[]'This is where people reach for `as string` to shut TypeScript up. Don't. Use `satisfies` from the start and the problem doesn't exist.
When 'as' Is Still Appropriate
We're not here to tell you `as` is always wrong. There are legitimate uses. The honest answer is: use `as` when you have information that TypeScript genuinely can't infer, not when you're trying to avoid a type error you should fix properly.
- Narrowing after runtime checks TypeScript can't see — like values from external APIs where you've validated the shape yourself
- DOM queries: `document.getElementById('my-input') as HTMLInputElement` when you know the element exists and its type
- Converting between compatible types where TypeScript is being overly conservative — `unknown as string` after a type guard
- Test fixtures where you're intentionally creating partial objects: `{} as User` in a unit test setup
- Interacting with poorly typed third-party libraries where the types lie and you know better
The rule of thumb: if you're using 'as' to make a valid value fit a type, question whether satisfies would give you more. If you're using 'as' to tell TypeScript something it can't verify on its own — runtime data, DOM state, external API responses — then 'as' is fine.
Combining satisfies with const for Maximum Precision
One pattern we've settled into: `satisfies` pairs beautifully with `as const` when you want both literal types AND exhaustive validation. Without `as const`, string values are widened to `string`. With it, they stay as literals. Stack them:
type Status = 'active' | 'inactive' | 'pending';
type UserRole = {
label: string;
defaultStatus: Status;
permissions: readonly string[];
};
const USER_ROLES = {
admin: {
label: 'Administrator',
defaultStatus: 'active',
permissions: ['read', 'write', 'delete'] as const,
},
viewer: {
label: 'Viewer',
defaultStatus: 'pending',
permissions: ['read'] as const,
},
} as const satisfies Record<string, UserRole>;
// You get literal types for everything:
type RoleName = keyof typeof USER_ROLES;
// 'admin' | 'viewer'
type AdminPermissions = typeof USER_ROLES.admin.permissions;
// readonly ['read', 'write', 'delete']
// AND validation — try adding 'defaultStatus: "banned"'
// and TypeScript will catch it because 'banned' is not a valid Status
// Note: 'as const' goes before 'satisfies' — order matters
// 'satisfies' checks after 'as const' narrows the typeThe order matters: `as const` then `satisfies`. If you flip them, `satisfies` validates first (against the wider inferred type) and then `as const` narrows. You want the narrowing to happen first so `satisfies` checks the literal values against your type.
A Note on Next.js and Config Files
We use `satisfies` heavily in Next.js projects for things like navigation configs, theme objects, and metadata defaults. Any time you have a config object that drives your app AND you need to derive types from it elsewhere (for type-safe routing, for generating nav menus, for creating exhaustive switches), `satisfies` is the right tool. If you're using one of the peal.dev templates, you'll notice we've already applied this pattern in the route and config files — it's one of those details that makes working with the codebase feel solid instead of fragile.
The metadata pattern in Next.js App Router is another good example. You want to validate your metadata object matches Next.js's `Metadata` type, but you also want TypeScript to remember specific string values for reuse:
import type { Metadata } from 'next';
const siteConfig = {
name: 'My App',
description: 'The best app ever built at 2am',
url: 'https://myapp.com',
ogImage: 'https://myapp.com/og.png',
} as const;
export const metadata = {
title: {
default: siteConfig.name,
template: `%s | ${siteConfig.name}`,
},
description: siteConfig.description,
metadataBase: new URL(siteConfig.url),
openGraph: {
title: siteConfig.name,
description: siteConfig.description,
url: siteConfig.url,
siteName: siteConfig.name,
images: [{ url: siteConfig.ogImage }],
type: 'website',
},
} satisfies Metadata;
// Validates the whole object against Next.js's Metadata type
// but keeps the precise inferred types for anything you derive from itThe Practical Decision Tree
When you're staring at a type error and reaching for a solution, here's how to think about it:
- You're defining a constant/config object and want to ensure it matches a type → use satisfies
- You want to derive types from the object later (keyof, typeof, mapped types) → use satisfies
- TypeScript can't know the type at compile time (API response, DOM query, runtime data) → use as, but add a runtime check if the stakes are high
- You're annotating a variable that will be reassigned → use a type annotation (: Type) instead of either
- You need both literal precision AND shape validation → use as const satisfies Type
- You're in a test and just want to stop TypeScript complaining → as is fine, tests are supposed to cheat a bit
The mental model: 'as' says 'trust me'. A type annotation says 'this is the type going forward'. 'satisfies' says 'check that this fits, but remember what it actually is'. Most config objects want the third option.
TypeScript 4.9 shipped `satisfies` in November 2022, which means if you're on anything recent you have it available. There's no reason to keep reaching for `as` on config objects and constants. The next time you write `as SomeConfigType`, pause for two seconds and ask if `satisfies SomeConfigType` would catch more bugs while keeping better types. The answer is usually yes, and it costs you zero extra keystrokes.
