cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Literal types

Make a type that is one exact value, like "small", join literals into a list of allowed options, and see why let widens them and as const keeps them.

By the end, you can
  • 01
    Write literal typesUse exact strings, numbers, and booleans as types, and join them into options like "small" | "medium" | "large".
  • 02
    Predict wideningSay when TypeScript keeps an exact value (const) and when it widens to string or number (let, object properties).
  • 03
    Keep values exactUse as const, a written type, or a check like sort === "price" to pass exact values where options are expected.

A type that is one exact value

A shop sells T-shirts in three sizes: small, medium, and large. The page keeps the chosen size as text. But string allows any text at all, like "huge" or a typo like "meduim". TypeScript can be stricter. A type can be one exact value:

A size that can only be medium (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
let size: "medium" = "medium";size = "large";

Line 1 says the type of size is "medium". That isn't a value here; it's a type that allows exactly one value, the text "medium". A type like this is called a literal type, because it's written as the value itself (a value written straight into code, like "medium" or 3, is called a literal). Line 2 stores "large", and TypeScript marks it on purpose: Type '"large"' is not assignable to type '"medium"'. The double quotes inside single quotes are how TypeScript writes a literal type. In plain words: "this variable may only hold the word medium".

A variable that can hold only one value isn't much use. The useful part comes when you join literal types into a union, the "this or that" type from the last lesson:

A size that is one of three words (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
let size: "small" | "medium" | "large" = "medium";//  ^?size = "large";console.log(size);size = "huge";

Line 1 says size is "small", "medium", or "large", and starts it as "medium". The query on line 2 shows that type. Line 3 changes it to "large", which is fine, and line 4 prints large. Line 5 tries "huge". It's text, but not one of the three words, so TypeScript marks it on purpose.

Try it yourself. Type a value for line 1 and watch the list under the code as you type.

Playground: type a T-shirt size
A size that must be one of three wordsPop out in the code editor (opens in a new tab)TypeScript
let size: "small" | "medium" | "large" = "huge";console.log(size);
Line 1 holds"huge"
What happens
TypeScript1 error: line 1
When it runsprints huge
Try it yourself

"huge" is text, but not one of the three words the type allows. TypeScript says Type '"huge"' is not assignable to type '"small" | "medium" | "large"'. Notice the quotes inside the quotes: '"huge"' is the literal type of that one word.

The code is checked by the real TypeScript compiler each time you type. Only line 1 changes. Reset puts "huge" back.

"large" and "small" have no errors. "huge" and the empty text "" are strings, but not options. "Large" shows that capital letters count, and TypeScript suggests the word you probably meant.

A shop window with three size buttons: small, medium, and large. Above it, the type size: "small" | "medium" | "large". Outside the window, the word huge, marked not an option.size: "small" | "medium" | "large"shop"small""medium""large"the three options"huge"not an option
The type lists three exact words, like the size buttons on a shop page. "huge" is text too, but it isn't one of the three, so TypeScript turns it away.
Real-life analogyThe cup sizes on a cafe menu

A cafe's menu says cups come in small, medium, or large. You can order any one of those. Ask for a huge cup and the barista tells you it isn't on the menu. You don't have to remember the sizes, either: the menu is right there by the till.

In real life: The menu lists small, medium, and large
In TypeScript: size: "small" | "medium" | "large"
In real life: You order a medium
In TypeScript: size = "medium" is allowed
In real life: You ask for a huge cup, and the barista says it isn't on the menu
In TypeScript: size = "huge" gets an error
In real life: The menu hangs by the till, so you see the choices before you order
In TypeScript: The editor lists the three options as you type
The main idea

A literal type is a type that allows one exact value, like "medium" or 5. Joined into a union, literal types make a list of allowed options: "small" | "medium" | "large". Any other value gets an error, even if it's the same kind of value.

Exact numbers and booleans

Numbers can be literal types too. A product review has a star rating from 1 to 5:

A star rating from 1 to 5 (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
let rating: 1 | 2 | 3 | 4 | 5 = 5;rating = 4;rating = 6;

Line 1 says rating is one of the five numbers 1 to 5, and starts it at 5. Line 2 changes it to 4, which is fine. Line 3 tries 6, and TypeScript marks it on purpose: Type '6' is not assignable to type '1 | 2 | 3 | 4 | 5'. Six is a number, but not one of the five.

Booleans have literal types as well. There are only two: true and false. In fact, boolean is just a short name for true | false:

false as a type, and boolean as true | falsePop out in the code editor (opens in a new tab)TypeScript
const done = false;//    ^?let open: true | false = false;//  ^?

Line 1 makes a const called done. The query on line 2 shows const done: false: the literal type false, because a const can't change (the next section explains this). Line 3 writes the type true | false, and the query on line 4 shows let open: boolean. TypeScript writes that union with its short name.

Literal types also work for what a function gives back. A return type is written after the brackets of the parameters. A function that compares two prices for sorting can promise to give back only -1, 0, or 1:

A compare function that returns -1, 0, or 1Pop out in the code editor (opens in a new tab)TypeScript
function comparePrices(a: number, b: number): -1 | 0 | 1 {  if (a < b) return -1;  if (a > b) return 1;  return 0;} console.log([4, 3, 5].sort(comparePrices).join(", "));

Line 1 ends with : -1 | 0 | 1, the return type. Line 2 returns -1 when a is cheaper, line 3 returns 1 when it costs more, and line 4 returns 0 when they cost the same. Line 7 sorts three prices with it and prints 3, 4, 5. TypeScript checks every return against the promise:

A compare function that returns any number (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function comparePrices(a: number, b: number): -1 | 0 | 1 {  return a - b;} console.log([4, 3, 5].sort(comparePrices).join(", "));

Line 2 returns a - b, which can be any number, like 4 - 3 = 1 or 3 - 5 = -2. TypeScript marks it on purpose: Type 'number' is not assignable to type '0 | 1 | -1'. Notice that TypeScript lists the three numbers in its own order; the order in a union doesn't matter. JavaScript's sort would accept any number, so line 5 still prints 3, 4, 5, but the function no longer keeps its promise.

let, const, and the exact value

When you don't write a type, TypeScript works one out from the value. For text, it has two choices: the exact word, or any string. Which one it picks depends on whether the variable can change:

The same word in a const and a letPop out in the code editor (opens in a new tab)TypeScript
const size = "medium";//    ^?let otherSize = "medium";//  ^?

Line 1 stores "medium" in a const. The query on line 2 shows const size: "medium": a const can never hold anything else, so its exact value is its type. Line 3 stores the same word in a let, and the query on line 4 shows let otherSize: string. A let can change later, so TypeScript allows any text. Going from the exact value to the general type is called widening.

Left: const size = "medium". A box called size holds the card "medium", and its type is "medium", because it cannot change. Right: let otherSize = "medium". A box called otherSize holds the same card, but its type is string, because it can change later.const size = "medium""medium"sizetype: "medium"can't changelet otherSize = "medium""medium"otherSizetype: stringcan change later
Both boxes hold the same word. A const can never change, so its type is the exact word, "medium". A let can change later, so TypeScript widens its type to string.

Widening usually helps: you can store a new word in otherSize later. But it matters when you pass the variable to something that takes only certain words. Press each button to declare choice a different way on line 5:

Playground: let, const, or a written type
A size chosen in a variablePop out in the code editor (opens in a new tab)TypeScript
function setSize(size: "small" | "medium" | "large") {  console.log(`Size: ${size}`);} let choice = "large";setSize(choice);
Console outputSize: large
What happens
Type of choicestring
TypeScript1 error: line 6
Try it yourself

let can change later, so TypeScript gives choice the wide type string. Line 6 then passes any string to a function that takes three words, and TypeScript says Argument of type 'string' is not assignable to parameter of type '"small" | "medium" | "large"'. It still prints Size: large when it runs.

TypeScript checks the code for real, and the output is what the code prints. Each button changes only line 5. Reset goes back to let.

With let, choice is a string, and line 6 gets an error, because any string might not be a size. With const, it's exactly "large", which fits. With let with a type, you said which words it may hold, so it fits and can still change to another size. const, wrong size shows the other side: an exact type also lets TypeScript catch the wrong word.

Object properties widen too, even in a const object:

An order whose size is just a string (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function setSize(size: "small" | "medium" | "large") {  console.log(`Size: ${size}`);} const order = { item: "Tea", size: "medium" };//    ^?setSize(order.size);

Lines 1 to 3 are the same setSize function. Line 5 makes an order with a size. The query on line 6 shows size: string, not "medium". The const only stops order from pointing to a different object; you can still write order.size = "huge", so TypeScript widens the property. Line 7 gets the same error as the let button: Argument of type 'string' is not assignable to parameter of type '"small" | "medium" | "large"'. The as const section below fixes this.

The table below sums up widening. Read each row as: when you write this, TypeScript gives this type, for this reason.

When TypeScript keeps the exact value, and when it widens
When you writeThe type TypeScript givesWhy
const size = "medium""medium"A const can't change, so its exact value is its type.
let size = "medium"stringA let can change later, so TypeScript allows any text.
let size: "small" | "medium" = "medium""small" | "medium"You wrote the type, so TypeScript uses it.
const order = { size: "medium" }{ size: string }A property can change, even in a const object.
const order = { size: "medium" } as const{ readonly size: "medium" }as const makes the property read-only and keeps its exact value.

Unions of literals as options

Frontend code is full of settings that take one of a few words: a button's style, a theme, a sort order, a text alignment. A union of literals turns those words into a checked list of options:

A button style that is one of three options (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function buttonClass(variant: "primary" | "secondary" | "danger") {  return `button button-${variant}`;} console.log(buttonClass("primary"));console.log(buttonClass("danger"));console.log(buttonClass("delete"));

Line 1 says variant is "primary", "secondary", or "danger". Line 2 builds a CSS class name from it. Line 5 prints button button-primary and line 6 prints button button-danger. Line 7 passes "delete", and TypeScript marks it on purpose: Argument of type '"delete"' is not assignable to parameter of type '"primary" | "secondary" | "danger"'. Plain JavaScript would print button button-delete, a class with no styles, and the button would look broken.

The list of options also helps while you type. Your editor knows them and offers them:

An editor window. After typing buttonClass and an opening quote, a list opens with danger, primary, and secondary. A note says the editor lists the three options.editorbuttonClass("dangerprimarysecondarythe editor liststhe three options
You type buttonClass(" and the editor lists the three words the parameter allows. It knows them from the type on line 1.

The table below compares the same function with variant: string and with the three options. Read each row as: this call, then what happens with each type.

A plain string versus a list of options
When you writeWith variant: stringWith the three options
buttonClass("primary")Fine.Fine.
buttonClass("delete")Fine, and the page gets a class that doesn't exist.An error: "delete" isn't an option.
buttonClass("Primary")Fine, and the button loses its style.An error: "Primary" isn't an option, because capital letters count.
Typing buttonClass("The editor has nothing to suggest.The editor lists the three options.

A union can mix literal types with other types. A width can be a number of pixels, or the word "auto":

A width that is a number or auto (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function setWidth(width: number | "auto") {  if (width === "auto") {    return "auto";  }  return `${width}px`;  //        ^?} console.log(setWidth(320));console.log(setWidth("auto"));console.log(setWidth("full"));

Line 1 says width is a number or the exact word "auto". Line 2 checks for the word with ===. TypeScript reads that check, as it read typeof checks in the last lesson: after line 3 returns, the only thing left is a number, and the query on line 6 shows (parameter) width: number. Line 9 prints 320px and line 10 prints auto. Line 11 passes "full" and gets an error on purpose; plain JavaScript would print fullpx.

as const keeps every value exact

Back to the order whose size widened to string. Writing as const after a value tells TypeScript: "this will never change, so keep every value exact":

An order with as const (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function setSize(size: "small" | "medium" | "large") {  console.log(`Size: ${size}`);} const order = { item: "Tea", size: "medium" } as const;//    ^?setSize(order.size);order.size = "large";

Line 5 ends with as const. The query on line 6 shows the new type: item is exactly "Tea", size is exactly "medium", and both are readonly, which means TypeScript won't let you change them. So line 7 is fine now and prints Size: medium. Line 8 tries to change the size, and TypeScript marks it on purpose: Cannot assign to 'size' because it is a read-only property. That's the rule behind as const: the values can't change, so their exact types are safe to keep.

It works on lists too. A list of the sizes the shop sells:

A list of sizes with as constPop out in the code editor (opens in a new tab)TypeScript
const sizes = ["small", "medium", "large"] as const;//    ^?console.log(sizes.length);

The query on line 2 shows readonly ["small", "medium", "large"]: a list that can't change, holding exactly those three words in that order. That's a read-only tuple, from the Arrays & tuples lesson. Line 3 prints 3. Like every type, as const is removed before the code runs; it changes what TypeScript allows, not what the code does. The as const & satisfies lesson goes further with it.

A sort menu that takes only known options

A product list has a sort menu with two choices: by price or by name. The sort function takes only those two words, so a typo gets an error:

Sort products by price or by namePop out in the code editor (opens in a new tab)TypeScript
function sortProducts(products: { name: string; price: number }[], by: "price" | "name") {  if (by === "price") {    return [...products].sort((a, b) => a.price - b.price);  }  return [...products].sort((a, b) => a.name.localeCompare(b.name));} const products = [  { name: "Tea", price: 3 },  { name: "Cake", price: 4 },  { name: "Bun", price: 2 },]; console.log(sortProducts(products, "price").map((product) => product.name).join(", "));console.log(sortProducts(products, "name").map((product) => product.name).join(", "));

Line 1 says by is "price" or "name". Line 2 checks for "price" and line 3 sorts cheapest first. Line 5 sorts by name for the other option. The [...products] copies the list first, so the original order stays (the Sorting arrays lesson explains why). Lines 8 to 12 make three products. Line 14 prints Bun, Tea, Cake and line 15 prints Bun, Cake, Tea.

The chosen option often arrives from outside, like the page address ?sort=price. That's plain text, so it isn't one of the options until you check:

A sort option read from the page addressPop out in the code editor (opens in a new tab)TypeScript
const params = new URLSearchParams("?sort=price");const sort = params.get("sort");//    ^?if (sort === "price" || sort === "name") {  console.log(`Sorting by ${sort}`);  //                        ^?}

Line 1 reads the address's settings with URLSearchParams, and line 2 gets the sort setting. The query on line 3 shows const sort: string | null: some text, or null when the address has no sort at all (the URL objects lesson covers this). Line 4 compares it with both options. Inside the if, the query on line 6 shows "price" | "name", so the value could go straight into sortProducts. Line 5 prints Sorting by price.

Error or fine?
  • let size: "small" | "large" = "small";
  • let size: "small" | "large" = "Small";
  • let rating: 1 | 2 | 3 = 3;
  • let rating: 1 | 2 | 3 = 4;
  • const size = "large"; const pick: "small" | "large" = size;
  • let size = "large"; const pick: "small" | "large" = size;
  • const order = { size: "large" }; const pick: "small" | "large" = order.size;
  • const order = { size: "large" } as const; const pick: "small" | "large" = order.size;
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

  • "A literal type is the same as the value." No. "medium" after a colon is a type that allows one value. Like every type, it's removed before the code runs.
  • "A const object keeps its exact values." No. Its properties can still change, so they widen to string or number. Add as const to keep them exact.
  • ""Small" and "small" are the same option." No. A literal type is exact, capital letters and spaces included.
  • "A let can never have a literal type." No. It widens only when you don't write a type. let choice: "small" | "large" = "large" keeps the options.
  • "Options in a union check what users type." No. Text from a form or the page address is a string. Compare it with each option first, as the sort example does.
  • "as const freezes my object." No. It only changes the types. The object can still be changed by code TypeScript doesn't check.

Practice exercises

Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.

Exercise 1 · Warm-upFind the option that isn't on the list

A notification setting can be all, mentions, or none. Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
let alerts: "all" | "mentions" | "none" = "all";
alerts = "none";
alerts = "some";
alerts = "mentions";

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

    Exercise 2 · Warm-upRead the type of a const

    What type does TypeScript give plan? Guess first, then point at it or add a // ^? line under it. Does nextPlan get the same type?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const plan = "pro";let nextPlan = "pro";

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

      Exercise 3 · PracticeTurn a string into a list of options

      CSS text alignment takes left, center, or right. With string, TypeScript can't catch a wrong word. Change line 1 to list the options. Which line does TypeScript mark then?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function setAlign(align: string) {  console.log(`text-align: ${align}`);} setAlign("left");setAlign("centre");

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

        Exercise 4 · PracticeKeep the picked size exact

        TypeScript marks line 6, even though the size is medium. Fix it. What does your fixed code print?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        function setSize(size: "small" | "medium" | "large") {  console.log(`Size: ${size}`);} let picked = "medium";setSize(picked);

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

          Exercise 5 · ChallengeSend a request with an exact method

          request takes the method "GET" or "POST". What type does saveCart.method have in the starter? Then fix line 5 so line 6 is accepted.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          function request(url: string, method: "GET" | "POST") {  console.log(`${method} ${url}`);} const saveCart = { url: "/api/cart", method: "POST" };request(saveCart.url, saveCart.method);

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

            Exercise 6 · ChallengeOpen the tab named in the address

            A product page reads the open tab from its address. TypeScript marks line 7. Fix it so a missing or unknown tab shows the details tab. What does your fixed code print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            function showTab(tab: "details" | "reviews") {  console.log(`Showing ${tab}`);} const params = new URLSearchParams("?tab=reviews");const tab = params.get("tab");showTab(tab);

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

              Check your understanding

              For each question, ask two things: is the type an exact value or a wide one like string, and could the value change later? The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The literal types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the type "small" | "medium" | "large" allow?

                Choose an answer to see the explanation.

              2. Question 2 of 7What type does TypeScript give the const named size?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const size = "large";

                Choose an answer to see the explanation.

              3. Question 3 of 7What type does TypeScript give the let named size?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                let size = "large";

                Choose an answer to see the explanation.

              4. Question 4 of 7Which line does TypeScript flag in this theme code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                let theme: "light" | "dark" = "light";
                theme = "dark";
                theme = "Dark";

                Choose an answer to see the explanation.

              5. Question 5 of 7Why does TypeScript mark line 6 of this settings code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function setTheme(theme: "light" | "dark") {
                  console.log(theme);
                }
                
                const settings = { theme: "dark" };
                setTheme(settings.theme);

                Choose an answer to see the explanation.

              6. Question 6 of 7What does as const do to { size: "medium" }?

                Choose an answer to see the explanation.

              7. Question 7 of 7This size 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
                function setSize(size: "small" | "medium" | "large") {
                  console.log(`Size: ${size}`);
                }
                
                setSize("huge");

                Choose an answer to see the explanation.

              Key takeaways

              • A literal type allows one exact value: "medium", 5, or true.
              • A union of literals is a list of options: "small" | "medium" | "large".
              • Options are exact: "Large" and "huge" get errors, and the editor suggests the right words.
              • A const keeps its exact value as its type. A let and object properties widen to string or number.
              • as const keeps every value exact and makes it read-only. A written type works too.
              • Text from outside, like the page address, is a string. Compare it with the options before you use it.

              Remember the one-liner.
              A literal type is one exact value; a union of them is a menu, and only what's on the menu gets through.

              Coming next: Type aliases & interfaces. Writing "small" | "medium" | "large" again and again is tiring; the next lesson gives a type a name, like Size, so you write it once.

              CompleteFrontend Clear concepts. Working examples.