satisfies and const Assertions When Inference Isn't Enough
Compare an annotation, a cast, satisfies and as const on the same config object. After this, you can check a value against a type and still keep the precise types the compiler worked out.
Course content
Sign in to save your progress.
-
Types That Compute
-
Conditional Types and infer Without Nested Nightmares
-
Template Literal Types for Routes and Event Names
-
-
-
Brands and Boundaries
-
Branded Types So IDs Don't Mix
-
satisfies and const Assertions When Inference Isn't Enough
-
-
-
Declare What JavaScript Didn't
-
Writing .d.ts for a Plain JS Module
-
Ambient Modules and Global Augmentation Without Breaking Everything
-
-
-
Projects That Scale
-
Project References and Solution-Style tsconfig
-
typescript-eslint Rules That Catch Real Mistakes
-
-
-
Migrate and Keep Moving
-
Move a JavaScript Folder to TypeScript Without a Big Bang
-
allowJs, checkJs and Leaving JSDoc Behind
-
Keep Build Times Sane: incremental and What Actually Slows tsc
-
-
- Final Check: TypeScript Advanced
Skip ahead: If you already use as const and know how satisfies differs from a type annotation, skip to "Combine them", then continue.
TypeScript's inference is good, until you give it a reason to be less specific. Write a type annotation on an object and the compiler forgets what you actually put in it. Write as SomeType and it stops checking. You end up choosing between a checked value with a vague type and a precise value that nobody checked. Two small features, satisfies and as const, remove that choice.
An annotation checks the shape and forgets the rest
type Config = { port: number | string; mode: "dev" | "prod"; hosts: string[] };
const config: Config = { port: 8080, mode: "dev", hosts: ["localhost"] };
console.log(config.port.toFixed(0));
annotation.ts(5,25): error TS2339: Property 'toFixed' does not exist on type 'string | number'.
Property 'toFixed' does not exist on type 'string'.
The object is fine. But the variable's type is Config, so config.port is a string | number as far as the compiler is concerned, even though you can see an 8080 right there. The annotation told TypeScript to treat the value as the wide type from that line on.
as hides mistakes
The other common reaction is to cast.
type Config = { port: number | string; mode: "dev" | "prod"; hosts: string[] };
const config = { port: 8080, mode: "dev" } as Config;
console.log(config.hosts.length);
This compiles. hosts is missing, and the cast told the compiler to believe you. At run time config.hosts is undefined and the next line throws a TypeError. A type assertion is not a check. It is a claim, and the compiler accepts claims unless they are wildly wrong.
satisfies checks and keeps the precise type
satisfies goes after the value. The compiler verifies that the value fits the type, then keeps the type it inferred from the value.
type Config = { port: number | string; mode: "dev" | "prod"; hosts: string[] };
const config = {
port: 8080,
mode: "dev",
hosts: ["localhost"],
} satisfies Config;
console.log(config.port.toFixed(0));
console.log(config.mode);
8080
dev
config.port is a number now, so toFixed is available. The check is still there, and it catches what the cast hid.
type Config = { port: number | string; mode: "dev" | "prod"; hosts: string[] };
const missing = { port: 8080, mode: "dev" } satisfies Config;
const typo = { port: 8080, mode: "dve", hosts: [] } satisfies Config;
const extra = { port: 8080, mode: "dev", hosts: [], debug: true } satisfies Config;
satisfies_bad.ts(3,45): error TS2741: Property 'hosts' is missing in type '{ port: number; mode: "dev"; }' but required in type 'Config'.
satisfies_bad.ts(4,28): error TS2322: Type '"dve"' is not assignable to type '"dev" | "prod"'.
satisfies_bad.ts(5,53): error TS2353: Object literal may only specify known properties, and 'debug' does not exist in type 'Config'.
A missing property, a misspelled literal and an unknown property all fail. That last one is worth noticing. Excess property checks apply to satisfies just as they do to an annotation, which is what a cast gives up.
as const freezes the literal
as const tells the compiler to infer the narrowest type for a literal: strings stay literal, arrays become readonly tuples and object properties become readonly.
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number];
function canWrite(role: Role): boolean {
return role !== "viewer";
}
console.log(canWrite("editor"));
console.log(ROLES.includes("admin"));
canWrite("owner");
ROLES.push("owner");
as_const.ts(10,10): error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
as_const.ts(11,7): error TS2339: Property 'push' does not exist on type 'readonly ["admin", "editor", "viewer"]'.
One array is now both the list you loop over and the source of the Role type. Add "owner" to it and Role grows. You no longer keep a union and an array in sync by hand, which is exactly the kind of duplication that drifts.
as const only works on literal values. It cannot freeze a variable that already exists.
const names = ["a", "b"];
const frozen = names as const;
const_variable.ts(2,16): error TS1355: A 'const' assertion can only be applied to references to enum members, or string, number, boolean, array, or object literals.
Combine them
When you want both, write both. The value is checked against a looser type, and the literal types survive.
const routes = {
home: "/",
profile: "/users/me",
settings: "/settings",
} as const satisfies Record<string, `/${string}`>;
type RouteName = keyof typeof routes;
type RoutePath = (typeof routes)[RouteName];
function link(name: RouteName): RoutePath {
return routes[name];
}
console.log(link("profile"));
/users/me
The satisfies Record part uses a template literal type for the values, so any path that does not start with a slash is rejected. as const keeps "/users/me" as a literal, so RoutePath is the union of the three real paths, not string. The template literal type from the first section is doing real work here: the check is on the shape of the text.
💡 Tip: Read satisfies as "must fit this". Read : Type as "treat as this". Read as as "trust me". Choose the weakest promise that gets the job done, and that is satisfies most of the time.
Common mistakes
- Using
as constto silence a readonly error you do not understand. If a function needs to modify the array, give it a copy. Do not make everything readonly and then cast it back. - Reaching for
satisfieswhen you need the declared type to be visible to callers. An exported function parameter should use a plain annotation, because the annotation is the contract. - Forgetting that
as constmakes nested arrays and objects readonly all the way down. A function that takesstring[]will refuse areadonly string[], and the fix is to declare the parameter asreadonly string[].
⚠ Warning: satisfies does not change the type of the value. It is not a conversion. It checks and gets out of the way. If a call site then needs a different type, you still need a real function or a parse step.
If you skip this, you will keep picking between a precise value nobody verified and a verified value the compiler cannot see through. Which config object in your project is annotated, and what is the single property you wish the compiler still remembered?
About
Compare an annotation, a cast, satisfies and as const on the same config object. After this, you can check a value against a type and still keep the precise types the compiler worked out.
- Duration
- 10 min
- Last update
- 09/29/2026
Comments (0)
Sign in to commentNo comments yet.