Skip to Content
TypeScript Advanced: Types That Hold Up in Production
Advanced
0 likes
Courses
TypeScript Advanced: Types That Hold Up in Production

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.

Course Home

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 const to 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 satisfies when 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 const makes nested arrays and objects readonly all the way down. A function that takes string[] will refuse a readonly string[], and the fix is to declare the parameter as readonly 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.