AniUI Academy

as const and satisfies

Lock a value down to its most literal type with as const, and check a value against a type without widening or losing its inferred literal shape using satisfies.

9 min read

Two closely related, commonly confused tools that both start from the same observation: sometimes the type TypeScript infers by default is wider than the one you actually want to keep.

Reminder: widening

From the basic types lesson: a let binding widens a literal value to its general type, because it might be reassigned.

let status = "pending"; // inferred as string, not "pending"

This is usually the right default — but sometimes you deliberately want the narrower, literal type kept, even though the variable technically could be reassigned. That's what as const is for.

as const on a single value

Appending as const overrides widening, keeping the most literal type possible:

let status = "pending" as const; // inferred as "pending", not string
 
status = "done";
// Type '"done"' is not assignable to type '"pending"'.

This looks unhelpful in isolation — a variable that can only ever hold one value isn't very useful — but it becomes genuinely valuable on objects and arrays, where it applies recursively.

as const on objects and arrays

const config = {
  mode: "production",
  retries: 3,
};
// Without as const: { mode: string; retries: number }
 
const configLocked = {
  mode: "production",
  retries: 3,
} as const;
// With as const: { readonly mode: "production"; readonly retries: 3 }

Every property becomes readonly, and every value keeps its most specific literal type instead of widening to the general string/number. This matters constantly for discriminated unions and configuration objects, where "production" (a specific, checkable literal) is far more useful than string (which would silently accept "produciton", a typo, with no error).

Arrays get the tuple treatment too:

const directions = ["left", "right"];
// Without as const: string[]
 
const directionsLocked = ["left", "right"] as const;
// With as const: readonly ["left", "right"] — a tuple of exact literals

satisfies: checking without widening

satisfies validates a value against a type — the same job a type annotation does — but, critically, without changing what the value's own inferred type becomes afterward:

type Colors = Record<string, string>;
 
const palette = {
  primary: "#3b82f6",
  secondary: "#f59e0b",
} satisfies Colors;
 
palette.primary.toUpperCase(); // fine — palette.primary is known to be exactly "#3b82f6"'s type, string

Compare with a plain type annotation instead:

const paletteAnnotated: Colors = {
  primary: "#3b82f6",
  secondary: "#f59e0b",
};
// paletteAnnotated's type is now Colors — i.e., Record<string, string> —
// which has lost the fact that it specifically has "primary" and "secondary" keys.

With satisfies, palette keeps its own precise, inferred shape (an object with exactly primary and secondary, both known to be specific string values) while still having been checked against Colors at the point of declaration — so a typo'd key, or a value of the wrong type, is still caught immediately:

const bad = {
  primary: "#3b82f6",
  secondary: 42, // wrong type
} satisfies Colors;
// Type 'number' is not assignable to type 'string'.

Combining as const and satisfies

The two are frequently used together — as const to lock in the most literal possible types, satisfies to validate the result against a shape without discarding that specificity:

type Config = {
  mode: "production" | "development";
  retries: number;
};
 
const config = {
  mode: "production",
  retries: 3,
} as const satisfies Config;
 
// config.mode is the literal "production", validated to be a
// valid Config["mode"], not widened to the general Config type.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • as const overrides widening, keeping a value's (and, recursively, an object's or array's) most specific literal type instead of the general one.
  • satisfies validates a value against a type without replacing the value's own inferred type the way a plain annotation would.
  • With a plain annotation, a value's type becomes exactly the annotation; with satisfies, the value keeps its own precise shape, having merely been checked against the target.
  • The two combine well: as const satisfies SomeType locks in literal types and validates them against a shape at once.

Check yourself

4 questions · pass 3/4 to unlock Typing Asynchronous Code

up to 50
  1. 1.Given let status = "pending"; versus let status = "pending" as const;, what is the inferred type in each case?

  2. 2.What does const point = { x: 1, y: 2 } as const; change compared to without as const?

  3. 3.What does const config = { retries: 3 } satisfies Options; check, that const config: Options = { retries: 3 } does not preserve?

  4. 4.Why can satisfies catch a typo that a plain object literal (with no annotation at all) would miss?

4 left to answer