Lesson 23 of 25
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.
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 literalssatisfies: 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, stringCompare 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
What to remember
as constoverrides widening, keeping a value's (and, recursively, an object's or array's) most specific literal type instead of the general one.satisfiesvalidates 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 SomeTypelocks in literal types and validates them against a shape at once.
Check yourself
4 questions · pass 3/4 to unlock Typing Asynchronous Code
1.Given
let status = "pending";versuslet status = "pending" as const;, what is the inferred type in each case?2.What does
const point = { x: 1, y: 2 } as const;change compared to without as const?3.What does
const config = { retries: 3 } satisfies Options;check, thatconst config: Options = { retries: 3 }does not preserve?4.Why can satisfies catch a typo that a plain object literal (with no annotation at all) would miss?
4 left to answer