cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Place as const and follow it downPut as const after a whole value or one part, know it reaches nested objects and lists, and pass read-only lists to functions.
  • 02
    Check a value with satisfiesCatch a wrong value or a misspelled key while keeping the exact type TypeScript worked out, which an annotation would replace.
  • 03
    Choose the right toolPick an annotation, as const, satisfies, or as const satisfies for 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:

A theme setting TypeScript widened (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

The same setting with as constPop out in the code editor (opens in a new tab)TypeScript
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 const can 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 because as const checks nothing at all:
A typo as const keeps and satisfies catches (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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".

Left, with as const: the misspelled property fontSzie: 16 becomes the type readonly fontSzie: 16, kept with no check. Right, with satisfies Settings: the same property is checked against Settings and marked: did you mean fontSize?With as constfontSzie: 16you writereadonly fontSzie: 16kept, typo and allWith satisfies SettingsfontSzie: 16checked against SettingsDid you mean fontSize?caught where you wrote it
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.
Real-life analogyA chalkboard menu and a printed menu

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 theme is a string
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: theme is exactly "dark", and read-only
In real life: Before printing, the manager checks every dish against the kitchen's list
In TypeScript: satisfies Settings checks every property against the type
The main idea

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:

Playground: how settings is written
A theme settingPop out in the code editor (opens in a new tab)TypeScript
type Theme = "light" | "dark";function setTheme(theme: Theme) {  console.log(`Theme: ${theme}`);} const settings = { theme: "dark", fontSize: 16 };//    ^?setTheme(settings.theme);
Console outputTheme: dark
What happens
TypeScript1 error: line 8
When it runsthe same every time
Try it yourself

A 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".

TypeScript checks the code for real, and the query on line 7 shows the type it worked out. Each button changes only line 6. Reset goes back to Plain object.

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:

as const after a property (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

Three changes to a const shop (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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 for open.
  • Line 9 adds a tag: Property 'push' does not exist on type 'readonly ["tea", "cake"]'. as const turned 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 no push.

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.

An editor window marks the line shop.hours.open = 8 as read-only. An arrow labelled types removed leads to a console window where shop.hours.open gives 8: the value still changed.editorshop.hours.open = 8;read-onlyTypeScript says notypes removedconsoleshop.hours.open8the value still changed
The editor marks 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:

Object.freeze stops only the top levelPop out in the code editor (opens in a new tab)TypeScript
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.

as const and Object.freeze, side by side
Questionas constObject.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:

A read-only list passed to two functions (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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 prints 2.
  • 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 prints 2.
  • 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 prints 2.

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":

An annotation that hides the detail (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

The same settings with satisfiesPop out in the code editor (opens in a new tab)TypeScript
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.

Left, annotation: the value fontSize: 16 is checked against Settings and its type becomes fontSize: number | "auto". Right, satisfies: the same value is checked against Settings and keeps the type fontSize: number.AnnotationfontSize: 16checked against SettingsfontSize: number | "auto"replaced by SettingssatisfiesfontSize: 16checked against SettingsfontSize: numberits own type kept
Both check 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:

Playground: type the theme
Settings checked with satisfiesPop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme: "light" | "dark"; fontSize: number }; const settings = { theme: "blue", fontSize: 16 } satisfies Settings;//    ^?console.log(settings.theme);
The theme on line 3"blue"
What happens
TypeScript1 error: line 3
When it runsprints blue
Try it yourself

satisfies 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.

The code is checked by the real TypeScript compiler each time you type. Only the theme on line 3 changes. Reset puts "blue" back.

"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:

A satisfied theme can't switch (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

An annotation, as const, and satisfies, side by side
QuestionAnnotationas constsatisfies
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.

Playground: a shop's routes
The pages of a shopPop out in the code editor (opens in a new tab)TypeScript
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);//                      ^?
Console output/cart
What happens
TypeScriptNo errors
Type of the path"/cart"
Try it yourself

Every page has a path and a title, so satisfies Routes passes. as const kept every value exact, so the query shows path: "/cart".

TypeScript checks the code for real, and the output is what this code prints. Each button changes one line. Reset goes back to Every page.

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:

A shop with the checkout page left out (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

Error or fine?
  • 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";
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "as const freezes my object." No. It only changes what TypeScript allows. The shop's opening hour still changed to 8 when the code ran. Use Object.freeze to stop changes while the code runs.
  • "as const only affects the top level." No. It reaches nested objects and lists too. That's Object.freeze, which stops at one level.
  • "An as const list fits any list parameter." No. It's read-only, so it only fits a parameter that promises not to change it, like readonly Size[].
  • "as const works on any value." No. It goes right after a value written out in full. After a name or a call, it's error 1355.
  • "satisfies gives 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.
  • "satisfies is just another way to write as." No. as trusts you and checks almost nothing. satisfies checks the value fully, missing and extra properties included.
  • "satisfies is always better than an annotation." No. A value checked with satisfies keeps 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.

Exercise 1 · Warm-upRead a nested const type

What type does TypeScript give open? Guess first. Then point at open to check, or press Edit & run and add a // ^? line under it.

Starter codePop out in the code editor (opens in a new tab)TypeScript
const shop = { name: "Tea House", hours: { open: 9, close: 17 } } as const;const open = shop.hours.open;

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the line TypeScript marks

    Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const theme = { mode: "dark", fontSize: 16 } as const;
    console.log(theme.mode);
    theme.fontSize = 18;

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAccept a read-only list

      TypeScript marks line 7. Fix line 2 so listSizes takes the read-only list, without removing as const. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      type Size = "small" | "medium" | "large";function listSizes(sizes: Size[]) {  console.log(sizes.join(", "));} const menuSizes = ["small", "large"] as const;listSizes(menuSizes);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeCatch a typo with satisfies

        TypeScript reports nothing, but the font size is misspelled. Make TypeScript check the object, then fix it. Which name does TypeScript suggest?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Settings = { theme: "light" | "dark"; fontSize: number }; const settings = { theme: "dark", fontSzie: 16 };console.log(JSON.stringify(settings));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeKeep the detail an annotation loses

          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?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Badge = { label: string; color: string | null }; const saleBadge: Badge = { label: "Sale", color: "red" };console.log(saleBadge.color.toUpperCase());

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeCheck a config and keep its paths

            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?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            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;

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The as const and satisfies quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Why does TypeScript flag line 6 of this total?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function 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.

              2. Question 2 of 7This code has a type error. What does it print when it runs anyway?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const hours = { open: 9, close: 17 } as const;
                hours.open = 8;
                console.log(hours.open);

                Choose an answer to see the explanation.

              3. Question 3 of 7Which line of this shop does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const shop = { name: "Tea House", tags: ["tea"] } as const;
                console.log(shop.tags.length);
                shop.tags.push("cake");

                Choose an answer to see the explanation.

              4. Question 4 of 7What does satisfies Settings do to the variable's type?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which line of the two settings does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type 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.

              6. Question 6 of 7Where can as const go?

                Choose an answer to see the explanation.

              7. 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 const stops 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 readonly parameter, like readonly Size[]. A list written in the call needs no as const.
              • as const is removed before the code runs, so it never stops a change. Object.freeze does, one level deep.
              • satisfies Type checks 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 Type gives 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.

              CompleteFrontend Clear concepts. Working examples.