as const & satisfies
Keep a settings object's exact values with as const, check it against a type with satisfies without losing them, and use both on a real routes config.
- 01Place as const and follow it downPut
as constafter a whole value or one part, know it reaches nested objects and lists, and pass read-only lists to functions. - 02Check a value with satisfiesCatch a wrong value or a misspelled key while keeping the exact type TypeScript worked out, which an annotation would replace.
- 03Choose the right toolPick an annotation,
as const,satisfies, oras const satisfiesfor a config object, and know none of them work while the code runs.
Exact values, and a check that keeps them
A shop page has two themes, light and dark. A function switches the theme, and it only accepts those two words. The page keeps its settings in an object:
type Theme = "light" | "dark";function setTheme(theme: Theme) { console.log(`Theme: ${theme}`);} const settings = { theme: "dark", fontSize: 16 };// ^?setTheme(settings.theme);Line 1 names the two themes: Theme is "light" | "dark", a union that is exactly one of those two words (the Union types lesson covers |). Lines 2 to 4 make setTheme, which takes a Theme. Line 6 makes the settings, with the theme "dark". Line 8 passes it to setTheme, and TypeScript marks it, on purpose here.
The query on line 7 shows why: theme: string. This is widening, from the Literal types lesson: an object's properties can change later, so TypeScript gives theme the type string instead of the literal type "dark", a type that is one exact value. The message on line 8, Argument of type 'string' is not assignable to parameter of type 'Theme', means "any text might not be one of your two themes". Literal types fixed this with as const after the object:
type Theme = "light" | "dark";function setTheme(theme: Theme) { console.log(`Theme: ${theme}`);} const settings = { theme: "dark", fontSize: 16 } as const;// ^?setTheme(settings.theme);The query on line 7 now shows readonly theme: "dark" and readonly fontSize: 16: exact values that code may read but not change. Line 8 has no error and prints Theme: dark. as const is a type assertion, from the Type assertions lesson, called a const assertion: instead of naming a type, it says "keep this value exactly as it is".
That's the recap. This lesson goes further in two directions:
- Where
as constcan go, and how far down it reaches: nested objects, lists, and lists you pass to functions. satisfies, which checks a value against a type and keeps its exact values. You need it becauseas constchecks nothing at all:
type Settings = { theme: "light" | "dark"; fontSize: number }; const fixed = { theme: "dark", fontSzie: 16 } as const;const checked = { theme: "dark", fontSzie: 16 } satisfies Settings;Line 1 says a Settings has a theme and a fontSize. Line 3 misspells the font size as fontSzie, and as const keeps the typo exactly, with no error, because nothing compares the object with Settings. Line 4 writes the same object with satisfies Settings after it. TypeScript checks it and marks line 4, on purpose: Object literal may only specify known properties, but 'fontSzie' does not exist in type 'Settings'. Did you mean to write 'fontSize'? means "Settings has no property with this name; you probably meant fontSize".
as const keeps whatever you wrote, typo included, because it checks nothing. satisfies Settings compares each name with Settings and marks fontSzie on the line where you wrote it.A cafe's chalkboard says "Soup of the day", and the staff wipe it and write a new soup each morning. You can't plan around one soup. The printed menu by the door says exactly "Tea, Cake", and nobody writes on it, so you can count on those words. Before it goes to the printer, the manager checks every dish against the list of what the kitchen can make, so a misspelled dish never gets printed.
- In real life: A chalkboard menu that staff rewrite every day
- In TypeScript: A plain object,
{ theme: "dark" } - In real life: You only know there will be some dish of the day
- In TypeScript: TypeScript only knows
themeis astring - In real life: A printed menu
- In TypeScript: The same object with
as const - In real life: It says exactly Tea and Cake, and nobody writes on it
- In TypeScript:
themeis exactly"dark", and read-only - In real life: Before printing, the manager checks every dish against the kitchen's list
- In TypeScript:
satisfies Settingschecks every property against the type
as const keeps a value's exact values: "dark" stays "dark" instead of widening to string, and every property becomes read-only. satisfies checks a value against a type and keeps the type TypeScript worked out. Both are removed before the code runs.
Where as const can go
Literal types put as const after a whole object and after a whole list. It can also go after one value inside an object, and an annotation is another way to get line 8 working. Press each button and watch the query on line 7:
type Theme = "light" | "dark";function setTheme(theme: Theme) { console.log(`Theme: ${theme}`);} const settings = { theme: "dark", fontSize: 16 };// ^?setTheme(settings.theme);Theme: dark1 error: line 8the same every timeA plain object. TypeScript widens "dark" to string, because a property can change later. Line 8 gets Argument of type 'string' is not assignable to parameter of type 'Theme': any text isn't the same as "light" or "dark".
Plain object widens theme to string, and line 8 gets the error. as const on the object keeps both values exact and makes both read-only. as const on theme only keeps just "dark" exact; fontSize stays a number and can change. A written type checks "dark" against Theme and keeps Theme, so later code could switch to "light". Every version prints Theme: dark: only the types differ.
as const only goes right after a value you write out in full: a string, a number, a boolean, a list, or an object. It can't go after a name or a call:
const settings = { theme: "dark" };const theme = settings.theme as const;Line 1 makes a plain object, so settings.theme is already a string. Line 2 tries settings.theme as const, and TypeScript says A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals. In plain words: "put as const where the value is written, not after a name". A literal is a value written out in the code, like "dark" or { theme: "dark" }. By line 2 the exact value is already lost, so the fix belongs on line 1.
Read-only, all the way down
as const makes every property read-only, and it reaches inside: nested objects and lists become read-only too. Here is a shop's details, with three changes TypeScript marks on purpose:
const shop = { name: "Tea House", hours: { open: 9, close: 17 }, tags: ["tea", "cake"],} as const; shop.name = "Cake House";shop.hours.open = 8;shop.tags.push("coffee");console.log(shop.hours.open, shop.tags.length);- Line 7 changes the name:
Cannot assign to 'name' because it is a read-only property. - Line 8 changes a property inside
hours, an object inside the shop, and gets the same message foropen. - Line 9 adds a tag:
Property 'push' does not exist on type 'readonly ["tea", "cake"]'.as constturned the list into a read-only tuple: exactly these items, in this order (the Arrays & tuples lesson covers tuples). A read-only list has no methods that change it, so there's nopush.
Now press Edit & run. Line 10 prints 8 3: the opening hour really changed to 8, and the list really has three tags. Like every type, as const is removed before the code runs, so nothing is left to stop the changes.
shop.hours.open = 8, because as const made it read-only. But as const is removed with the types, so when the code runs, the change happens and the console shows 8.JavaScript has its own way to stop changes while the code runs: Object.freeze (the Freezing and sealing lesson covers it). It works one level deep:
const shop = Object.freeze({ name: "Tea House", hours: { open: 9 } });// ^?shop.hours.open = 8;console.log(shop.hours.open);Line 1 freezes the shop. The query on line 2 shows Readonly<…> around the shop's type, which means its top-level properties are read-only (Partial, Pick, Omit & friends covers Readonly later). The top-level name even stays exactly "Tea House", because it can't change. But hours inside is an ordinary { open: number; }. So line 3 gets no error, and line 4 prints 8: the nested object wasn't frozen.
The table puts the two side by side. Read each row as a question, then each tool's answer.
| Question | as const | Object.freeze |
|---|---|---|
| When does it work? | While TypeScript checks your code. It's removed before the code runs. | While the code runs. It's a real JavaScript function. |
| How deep does it go? | All the way down: nested objects and lists are read-only too. | One level: shop.hours.open can still change. |
| What does a change do when the code runs? | It happens. Nothing is left to stop it. | A change to a top-level property fails: it's ignored, or throws a TypeError in strict mode. |
| Does TypeScript mark the change? | Yes, at every level. | Yes, but only for top-level properties. |
Read-only lists reach functions too. A parameter with a plain list type, like Size[], lets the function change the list it gets, with push for example. So TypeScript won't pass it a read-only list:
type Size = "small" | "medium" | "large";function countSizes(sizes: Size[]) { return sizes.length;}function countReadonly(sizes: readonly Size[]) { return sizes.length;} const sizes = ["small", "large"] as const;// ^?console.log(countReadonly(sizes));console.log(countSizes(sizes));console.log(countSizes(["small", "large"]));Line 1 lists the sizes. Lines 2 to 4 make countSizes, which takes a Size[]. Lines 5 to 7 make countReadonly, which takes a readonly Size[]: a list the function promises not to change. Line 9 makes the sizes with as const, and the query on line 10 shows readonly ["small", "large"]. Then:
- Line 11 passes the sizes to
countReadonly. No error, and it prints2. - Line 12 passes them to
countSizes, and TypeScript marks it, on purpose. The second line of the message says why:The type 'readonly ["small", "large"]' is 'readonly' and cannot be assigned to the mutable type 'Size[]'. "Mutable" means "can be changed". When it runs, it still prints2. - Line 13 writes the list right in the call. It needs no
as const: the parameter's type,Size[], already tells TypeScript to keep"small"and"large"exact. It prints2.
Without as const on line 9, sizes would be a string[], and neither function would take it, because any text isn't a Size. So for a fixed list you pass around, write as const, and give the functions that only read it a readonly parameter.
satisfies: check a value, keep its detail
as const keeps values exact, but it checks nothing. To check settings against a type, you could write an annotation. Here the font size may be a number or the word "auto":
type Settings = { theme: "light" | "dark"; fontSize: number | "auto" }; const settings: Settings = { theme: "dark", fontSize: 16 };// ^?console.log(settings.fontSize.toFixed(1));Line 1 says a Settings has a theme and a font size that is a number or "auto". Line 3 annotates settings with it. The query on line 4 shows const settings: Settings: the annotation replaced the type. So on line 5, TypeScript has forgotten that the font size is 16. It might be "auto", and text has no toFixed, so it says Property 'toFixed' does not exist on type 'number | "auto"'.
satisfies does the check without replacing the type. You write it after the value: value satisfies Type.
type Settings = { theme: "light" | "dark"; fontSize: number | "auto" }; const settings = { theme: "dark", fontSize: 16 } satisfies Settings;// ^?console.log(settings.fontSize.toFixed(1));Line 3 ends with satisfies Settings. TypeScript checks the object against Settings, then keeps the type it worked out. The query on line 4 shows theme: "dark" and fontSize: number. The theme stays exact because Settings lists exact themes. Line 5 has no error and prints 16.0.
fontSize: 16 against Settings. The annotation then replaces the type with Settings, so fontSize might be "auto". satisfies keeps the type TypeScript worked out: a number.The check is real. Type a theme and watch the list under the code:
type Settings = { theme: "light" | "dark"; fontSize: number }; const settings = { theme: "blue", fontSize: 16 } satisfies Settings;// ^?console.log(settings.theme);"blue"1 error: line 3prints bluesatisfies Settings checks the object, and "blue" isn't one of the two themes: Type '"blue"' is not assignable to type '"light" | "dark"'. The page would still print blue, because nothing checks while the code runs.
"blue" isn't one of the two themes, so satisfies reports it. "dark" and "light" pass, and the query on line 4 shows the exact one you typed. "Dark" fails too, and TypeScript asks whether you meant "dark". Leave out the quotes, and dark becomes a name that doesn't exist.
Keeping the exact type has one cost. The variable keeps theme: "dark", not Theme, so it can't switch themes later:
type Settings = { theme: "light" | "dark"; fontSize: number }; const settings = { theme: "dark", fontSize: 16 } satisfies Settings;settings.theme = "light";Line 3 checks the settings with satisfies. Line 4 switches to "light", which Settings allows, but TypeScript says Type '"light"' is not assignable to type '"dark"'. The variable's type is what it held, not what Settings allows. For a value that changes later, an annotation is the better fit.
The table compares the three tools. Read each row as a question, then how an annotation, as const, and satisfies answer it for the settings.
| Question | Annotation | as const | satisfies |
|---|---|---|---|
| Is the value checked against a type? | Yes. | No. | Yes. |
Does theme keep the exact value "dark"? | No. It gets the type you wrote, Theme. | Yes. | Yes, when the type lists exact values. |
Can later code change theme? | Yes, to any Theme. | No. Every property is read-only. | Only to the exact type it kept, so "light" is an error. |
| What's left when the code runs? | Nothing. It's removed. | Nothing. It's removed. | Nothing. It's removed. |
Config objects with satisfies
Apps keep a lot of fixed data in config objects: the pages of a site, the colors of a theme, the options in a menu. You want two things from TypeScript. First, a check that every entry is complete and spelled right. Second, the exact values, so your editor shows which path each page has. as const satisfies does both: as const keeps the values exact, then satisfies checks the result.
type Route = { path: string; title: string };type Routes = { home: Route; cart: Route; checkout: Route }; const routes = { home: { path: "/", title: "Home" }, cart: { path: "/cart", title: "Your cart" }, checkout: { path: "/checkout", title: "Checkout" },} as const satisfies Routes; console.log(routes.cart.path);// ^?/cartNo errors"/cart"Every page has a path and a title, so satisfies Routes passes. as const kept every value exact, so the query shows path: "/cart".
Lines 1 and 2 describe a shop's three pages: each Route has a path and a title. Lines 4 to 8 list them, and line 8 ends with as const satisfies Routes. With Every page, the query on line 11 shows path: "/cart". Without as const, the check still passes, but the path widens to string. A misspelled page and A title left out each get an error on the line that changed. Every version prints /cart. The order matters: as const must come right after the object, so satisfies Routes as const is error 1355.
A page left out is caught too:
type Route = { path: string; title: string };type Routes = { home: Route; cart: Route; checkout: Route }; const routes = { home: { path: "/", title: "Home" }, cart: { path: "/cart", title: "Your cart" },} as const satisfies Routes;Lines 4 to 7 list only home and cart. TypeScript marks line 7 with a long message that ends does not satisfy the expected type 'Routes', and the next line of the message says what's wrong: Property 'checkout' is missing.
const sizes = ["small", "large"] as const; const list: readonly string[] = sizes;const theme = { mode: "dark" } as const; theme.mode = "light";const sizes = ["small", "medium"] as const; sizes.push("large");const roll = Math.random() as const;const settings = { theme: "dark" } satisfies { theme: "light" | "dark" };const settings = { theme: "blue" } satisfies { theme: "light" | "dark" };const settings = { theme: "dark", size: 2 } satisfies { theme: string };const settings = { theme: "dark" } satisfies { theme: string }; settings.theme = "light";
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
- "
as constfreezes my object." No. It only changes what TypeScript allows. The shop's opening hour still changed to 8 when the code ran. UseObject.freezeto stop changes while the code runs. - "
as constonly affects the top level." No. It reaches nested objects and lists too. That'sObject.freeze, which stops at one level. - "An
as constlist fits any list parameter." No. It's read-only, so it only fits a parameter that promises not to change it, likereadonly Size[]. - "
as constworks on any value." No. It goes right after a value written out in full. After a name or a call, it's error 1355. - "
satisfiesgives the variable the type I wrote." No. It checks against that type, then keeps the type TypeScript worked out. An annotation gives the variable your type. - "
satisfiesis just another way to writeas." No.astrusts you and checks almost nothing.satisfieschecks the value fully, missing and extra properties included. - "
satisfiesis always better than an annotation." No. A value checked withsatisfieskeeps its exact type, so it can't switch from"dark"to"light"later. For values that change, use an annotation.
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code to try a fix: the errors and the // ^? answers update as you type, and the console shows what it prints.
What type does TypeScript give open? Guess first. Then point at open to check, or press Edit & run and add a // ^? line under it.
const shop = { name: "Tea House", hours: { open: 9, close: 17 } } as const;const open = shop.hours.open;const shop = { name: "Tea House", hours: { open: 9, close: 17 } } as const;const open = shop.hours.open;// ^?as const reaches every level, so shop.hours.open is the literal type 9, not number. The query on line 3 shows const open: 9.
Which line will TypeScript mark?
const theme = { mode: "dark", fontSize: 16 } as const;
console.log(theme.mode);
theme.fontSize = 18;const theme = { mode: "dark", fontSize: 16 };console.log(theme.mode);theme.fontSize = 18;Line 3 changes fontSize, which as const made read-only, so TypeScript reports Cannot assign to 'fontSize' because it is a read-only property. If the font size has to change, leave out as const, as here.
TypeScript marks line 7. Fix line 2 so listSizes takes the read-only list, without removing as const. What does the fixed code print?
type Size = "small" | "medium" | "large";function listSizes(sizes: Size[]) { console.log(sizes.join(", "));} const menuSizes = ["small", "large"] as const;listSizes(menuSizes);type Size = "small" | "medium" | "large";function listSizes(sizes: readonly Size[]) { console.log(sizes.join(", "));} const menuSizes = ["small", "large"] as const;listSizes(menuSizes);as const made menuSizes a read-only tuple. With readonly Size[], listSizes promises not to change the list, so line 7 is fine and prints small, large. Removing as const instead would make menuSizes a string[], which isn't a list of sizes.
TypeScript reports nothing, but the font size is misspelled. Make TypeScript check the object, then fix it. Which name does TypeScript suggest?
type Settings = { theme: "light" | "dark"; fontSize: number }; const settings = { theme: "dark", fontSzie: 16 };console.log(JSON.stringify(settings));type Settings = { theme: "light" | "dark"; fontSize: number }; const settings = { theme: "dark", fontSize: 16 } satisfies Settings;console.log(JSON.stringify(settings));With satisfies Settings, TypeScript reports 'fontSzie' does not exist in type 'Settings'. Did you mean to write 'fontSize'?. Fixing the name makes the check pass, and the code prints {"theme":"dark","fontSize":16}.
TypeScript says 'saleBadge.color' is possibly 'null', though the color is plainly "red". Fix line 3 so the badge is still checked. What does it print?
type Badge = { label: string; color: string | null }; const saleBadge: Badge = { label: "Sale", color: "red" };console.log(saleBadge.color.toUpperCase());type Badge = { label: string; color: string | null }; const saleBadge = { label: "Sale", color: "red" } satisfies Badge;console.log(saleBadge.color.toUpperCase());satisfies Badge still checks the badge, but keeps the type TypeScript worked out, where color is a string. So toUpperCase is allowed, and the code prints RED.
The routes are checked, but cartPath is only a string. Change line 7 so it's exactly the cart's path, and keep the check. What type does cartPath have now?
type Route = { path: string; title: string };type Routes = { home: Route; cart: Route }; const routes = { home: { path: "/", title: "Home" }, cart: { path: "/cart", title: "Your cart" },} satisfies Routes; const cartPath = routes.cart.path;type Route = { path: string; title: string };type Routes = { home: Route; cart: Route }; const routes = { home: { path: "/", title: "Home" }, cart: { path: "/cart", title: "Your cart" },} as const satisfies Routes; const cartPath = routes.cart.path;// ^?as const satisfies Routes keeps every value exact and checks the result, so the query on line 10 shows const cartPath: "/cart". Writing satisfies Routes as const instead is error 1355.
Check your understanding
For each question, ask what each tool does: as const keeps values exact and read-only, satisfies checks without replacing the type, and neither does anything when the code runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7Why does TypeScript flag line 6 of this total?
Read the code, then predictfunction total(prices: number[]) { return prices.reduce((sum, price) => sum + price, 0); } const prices = [3, 4] as const; console.log(total(prices));Choose an answer to see the explanation.
Question 2 of 7This code has a type error. What does it print when it runs anyway?
Read the code, then predictconst hours = { open: 9, close: 17 } as const; hours.open = 8; console.log(hours.open);Choose an answer to see the explanation.
Question 3 of 7Which line of this shop does TypeScript flag?
Read the code, then predictconst shop = { name: "Tea House", tags: ["tea"] } as const; console.log(shop.tags.length); shop.tags.push("cake");Choose an answer to see the explanation.
Question 4 of 7What does
satisfies Settingsdo to the variable's type?Choose an answer to see the explanation.
Question 5 of 7Which line of the two settings does TypeScript flag?
Read the code, then predicttype Settings = { theme: "light" | "dark"; fontSize: number | "auto" }; const a: Settings = { theme: "dark", fontSize: 16 }; const b = { theme: "dark", fontSize: 16 } satisfies Settings; console.log(a.fontSize.toFixed(1)); console.log(b.fontSize.toFixed(1));Choose an answer to see the explanation.
Question 6 of 7Where can
as constgo?Choose an answer to see the explanation.
Question 7 of 7You want a config checked against a type and its exact values kept. What do you write after the object?
Choose an answer to see the explanation.
Key takeaways
as conststops widening, as in Literal types. It goes right after a value written out in full, the whole object or one part of it, never after a name or a call.- It makes every level read-only: nested objects, and lists, which become read-only tuples.
- A read-only list only fits a
readonlyparameter, likereadonly Size[]. A list written in the call needs noas const. as constis removed before the code runs, so it never stops a change.Object.freezedoes, one level deep.satisfies Typechecks a value, missing, wrong, and extra properties included, then keeps the type TypeScript worked out.- An annotation replaces the type with yours. Use it for values that change later.
- For config objects,
as const satisfies Typegives you both: a full check and exact values.
Remember the one-liner.as const keeps the exact values; satisfies checks them without losing them.
Coming next: Structural typing. It starts a new stage on objects: TypeScript compares the shapes of objects, not their names, and any object with the right properties fits.