cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Union types

Let a value be text or a number with a union type like string | number, use it safely with a typeof check, and type parameters that accept either one.

By the end, you can
  • 01
    Write a unionUse A | B to say a value can be one of several types, and read what TypeScript lets in.
  • 02
    Use a union safelyKnow which operations work on every member, and check the type with typeof or in before using the rest.
  • 03
    Type real inputsAccept text or a number in a function parameter, and read union types TypeScript works out for results and lists.

One value, more than one type

A shop page shows an order. Older orders have a number for an ID, like 42. Newer ones arrive from the page address as text, like "A-42". In plain JavaScript, one variable can hold either. TypeScript isn't so sure:

An order ID that starts as a number (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
let orderId = 42;//  ^?orderId = "A-42";

Line 1 stores 42. Line 2 is a question for TypeScript: // ^? asks "what type is the name my ^ points at?", and the page writes the answer after it: let orderId: number. You didn't write a type, so TypeScript worked one out from the first value. Line 3 stores text, and TypeScript marks it, on purpose here: Type 'string' is not assignable to type 'number'. In plain words: "this variable holds numbers, and you're putting text in it".

You want to say "text or a number". That's what a union type is. You write the types with a | between them, and you read the | as "or":

An order ID that is text or a number (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
let orderId: string | number = 42;//  ^?orderId = "A-42";console.log(orderId);orderId = true;

Line 1 says orderId is a string | number: text or a number. It starts as 42, and the query on line 2 shows the type you wrote. Line 3 stores "A-42", and this time there's no error. Line 4 prints A-42. Line 5 tries true, which is a boolean (a true-or-false value). TypeScript marks it, on purpose: Type 'boolean' is not assignable to type 'string | number'. A boolean is neither of the two types the union lists. Each type in a union is called a member, so this union has two members.

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

Playground: type an order ID
An order ID that takes text or a numberPop out in the code editor (opens in a new tab)TypeScript
let orderId: string | number = true;console.log(orderId);
Line 1 holdstrue
What happens
TypeScript1 error: line 1
When it runsprints true
Try it yourself

true is a boolean, and the union only lets in text or numbers. TypeScript says Type 'boolean' is not assignable to type 'string | number'.

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

42 and "A-42" have no errors, because each one matches a member. true, null, and [42] each get an error, because none of them is text or a number. An empty box gets Expression expected, because line 1 needs a value.

A box called orderId with the sign string | number. The text "A-42" and the number 42 both go in. The value true, a boolean, stays outside: it does not fit.orderId: string | number"A-42"42fitorderIdtruedoesn't fita boolean
orderId takes text or a number. "A-42" and 42 both fit. true is a boolean, so TypeScript keeps it out.
Real-life analogyA cafe that takes cash or card

A small cafe has a sign by the till: "cash or card". You can pay with either, and the cashier takes it. A voucher is turned away, because it isn't on the sign. The cashier also knows that only cash gets change, so before counting change, they look at what you handed over.

In real life: The sign says: cash or card
In TypeScript: payment: string | number
In real life: You can pay with either one
In TypeScript: Any value of either member type is allowed
In real life: A voucher is turned away
In TypeScript: true gets an error: it isn't a member
In real life: To give change, the cashier first checks it's cash
In TypeScript: To use a string method, you first check it's a string
The main idea

A union type says a value can be one of several types. You write it with |, which reads "or": string | number. TypeScript lets you store a value of any member type, and it lets you use the value only in ways that work for every member, until you check which one you have.

Using a value that could be either

Storing a value in a union is easy: give it any member type. Using the value is where TypeScript gets careful. Here is a function that shows an order ID in capital letters:

A string method on a string or a number (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function showOrderId(orderId: string | number) {  console.log(orderId.toUpperCase());} showOrderId("a-42");

Line 1 says orderId is text or a number. Line 2 calls toUpperCase(), a method that only text has. Line 5 calls the function with "a-42", which works and prints A-42. But TypeScript marks line 2 on purpose, with two lines of message:

  • Property 'toUpperCase' does not exist on type 'string | number' means "you can't be sure this value has a toUpperCase". A property is anything you read with a dot, and methods count.
  • Property 'toUpperCase' does not exist on type 'number' names the member that lacks it. If someone calls showOrderId(42), the code throws a TypeError and stops.

The rule is simple: with a union, you can only do what every member can do. Try different things on line 2. Each button swaps that one line; the code then calls the function with text and with a number.

Playground: what works on both?
One function, called with text and with a numberPop out in the code editor (opens in a new tab)TypeScript
function showOrderId(orderId: string | number) {  console.log(orderId.toUpperCase());} showOrderId("a-42");showOrderId(42);
TypeScript1 error: line 2
Console output
Line 5A-42
Line 6TypeError: orderId.toUpperCase is not a function
Try it yourself

Only strings have toUpperCase. TypeScript says Property 'toUpperCase' does not exist on type 'string | number', and its second line names the member that lacks it: number. When it runs, the first call prints A-42, and the second, with 42, throws a TypeError.

TypeScript checks the code for real, and the console shows what the two calls print when the code runs. Each button changes only line 2. Reset goes back to toUpperCase().

toUpperCase() and toFixed(1) each belong to one member, so each gets an error, and each throws for the other member. .length gets an error too; plain JavaScript would quietly print undefined. + 1 gets an error because it adds to a number but joins to text. toString() and the template literal work for both, so there are no errors. The table below sums it up. Read each row as: this code, is it allowed on a string | number, and why.

What you can do with a string | number
What you writeAllowed?Why
orderId.toString()Yes.Strings and numbers both have toString.
String(orderId)Yes.String turns any value into text, so it works on both.
orderId.toUpperCase()No, until you check.Only strings have it. A number would throw a TypeError.
orderId.toFixed(1)No, until you check.Only numbers have it. Text would throw a TypeError.
orderId + 1No.It adds a number but joins text, so TypeScript won't guess which you meant.
orderId.toUpperCase() inside if (typeof orderId === "string")Yes.Inside the check, TypeScript knows orderId is a string.

To use a string method, you first check that you have a string. JavaScript's typeof operator gives the kind of a value as text, like "string" or "number" (the Checking types lesson covers it). TypeScript reads your typeof check:

Check with typeof, then use a string methodPop out in the code editor (opens in a new tab)TypeScript
function formatOrderId(orderId: string | number) {  if (typeof orderId === "string") {    return orderId.toUpperCase();    //     ^?  }  return "#" + orderId;  //           ^?} console.log(formatOrderId("a-42"));console.log(formatOrderId(42));

Line 2 asks whether orderId is text. Inside that if, the query on line 4 shows (parameter) orderId: string: TypeScript knows it's text there, so line 3 may call toUpperCase(). Line 3 also leaves the function with return, so line 6 runs only for numbers, and the query on line 7 shows (parameter) orderId: number. Line 10 prints A-42 and line 11 prints #42.

A card string | number on the left. A check, typeof orderId === "string", splits it in two. The yes path leads to a card string, where toUpperCase works, on line 3. The no path leads to a card number on line 6.string | numbertypeof orderId=== "string"yesnostring: toUpperCase()line 3number: "#" + orderIdline 6
The typeof check on line 2 splits the union. On the "yes" path, orderId is a string. On the other path, only number is left.

Turning a wide type into a more exact one with a check like this is called narrowing. TypeScript understands many checks, and the Narrowing lessons in the next stage, starting with Narrowing with typeof & truthiness, cover them one by one. For now, typeof is all you need.

Unions of object types

A union can join object types too. A search box asks a server for products. The server sends one of two replies: the products it found, or a message saying why it failed. Each reply is an object with its own properties:

Reading a property only one reply has (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Found = { status: number; results: string[] };type Failed = { status: number; message: string }; function showReply(reply: Found | Failed) {  console.log(reply.status);  console.log(reply.results);} showReply({ status: 404, message: "No products found" });

Lines 1 and 2 give names to two object types with type. type Found = … means "from now on, Found stands for this object type" (the Type aliases & interfaces lesson covers this). A Found reply has a status and a list of results. A Failed reply has a status and a message. Line 4 says reply is either one.

Line 5 reads reply.status, and that's fine: both replies have it. Line 6 reads reply.results, and TypeScript marks it on purpose: Property 'results' does not exist on type 'Found | Failed', and on its second line, Property 'results' does not exist on type 'Failed'. Line 9 shows why: a failed reply has no results, so line 5 prints 404 and line 6 prints undefined.

Two carts. Found holds status and results. Failed holds status and message. Both have status, so reading it is allowed. Only Found has results, so reading it without a check is an error.the search found productsstatusresultsFoundthe search failedstatusmessageFailedstatus is in both: allowedresults is in one: an error
A Found | Failed reply could be either cart. Both carts hold status, so you can read it. Only one holds results, so TypeScript wants a check first.

It's the same rule as before. A Found | Failed value could be either object, so you can read only the properties both have. To read the others, check which object you have. JavaScript's in operator asks whether an object has a property (Object basics covers it), and TypeScript reads that check too (the in & instanceof narrowing lesson goes further):

Check with in, then read the propertyPop out in the code editor (opens in a new tab)TypeScript
type Found = { status: number; results: string[] };type Failed = { status: number; message: string }; function showReply(reply: Found | Failed) {  if ("results" in reply) {    console.log(reply.results.join(", "));    //          ^?  } else {    console.log(reply.message);    //          ^?  }} showReply({ status: 200, results: ["Tea", "Cake"] });showReply({ status: 404, message: "No products found" });

Line 5 asks whether reply has a results property. Only Found has one, so inside the if, the query on line 7 shows (parameter) reply: Found, and line 6 may read the results. In the else, only Failed is left: the query on line 10 shows (parameter) reply: Failed, and line 9 reads the message. Line 14 prints Tea, Cake, and line 15 prints No products found.

Unions in parameters, results, and lists

Unions show up most in function parameters, where you want to accept more than one kind of input. A price might come from the server as a number, or from a form as text:

A function that takes a price as text or a number (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function formatPrice(price: string | number) {  return `$${Number(price).toFixed(2)}`;} console.log(formatPrice(4));console.log(formatPrice("4.5"));console.log(formatPrice(true));

Line 1 says formatPrice takes a price that is text or a number. Line 2 turns it into a number with Number(price), which works for both, and writes it with two decimals. Line 5 passes 4 and prints $4.00. Line 6 passes "4.5" and prints $4.50. Line 7 passes true, and TypeScript marks it on purpose: Argument of type 'boolean' is not assignable to parameter of type 'string | number'. An argument is a value you pass in a call, and a parameter is the name that receives it. Plain JavaScript would print $1.00, because Number(true) is 1.

TypeScript also gives you unions, when a value really could be one of two things. You met one in the Arrays & tuples lesson: find gives back a matching item, or undefined when nothing matches.

find gives back a number or undefinedPop out in the code editor (opens in a new tab)TypeScript
const prices = [3, 4, 5];const firstBig = prices.find((price) => price > 3);//    ^?console.log(firstBig);

Line 1 makes a list of prices. Line 2 looks for the first price above 3. The query on line 3 shows const firstBig: number | undefined: a number if one matches, undefined if none does. Line 4 prints 4. The null, undefined & strictNullChecks lesson shows how to handle the undefined part.

Lists can hold a union too. When a list mixes text and numbers, TypeScript works out a union for its items:

A list of sizes with text and numbers (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const sizes = [8, "M", 10];//    ^?sizes.push("L");sizes.push(true);

Line 1 makes a list of clothing sizes: 8, "M", and 10. The query on line 2 shows const sizes: (string | number)[], which means "a list where each item is text or a number". Line 3 adds "L", which is fine. Line 4 tries to add true, and TypeScript marks it on purpose: Argument of type 'boolean' is not assignable to parameter of type 'string | number'.

The round brackets in (string | number)[] matter. Without them, [] applies to number alone. The table below compares three types that look alike. Read each row as: this type, what it means, and whether it can hold the list [8, "M"].

Three list types that look alike
The typeWhat it meansCan it hold [8, "M"]?
(string | number)[]A list where each item can be text or a number.Yes.
string[] | number[]A list of only text, or a list of only numbers.No. It mixes the two in one list.
string | number[]One piece of text, or a list of numbers.No. Without round brackets, [] applies to number alone.

When a union gets long, you can put each member on its own line. A | is allowed before the first member, so every line starts the same way:

A union written one member per linePop out in the code editor (opens in a new tab)TypeScript
function setPageSize(  size:    | number    | string,) {  console.log(`Showing ${size} items`);} setPageSize(20);setPageSize("20");

Lines 2 to 4 say size is a number or a string, exactly like number | string on one line. Lines 9 and 10 both print Showing 20 items.

A search box that finds by id or name

A shop's search box lets people find a product by its number or by its name. One function handles both, with a union parameter and a typeof check:

Find a product by id or by namePop out in the code editor (opens in a new tab)TypeScript
const products = [  { id: 7, name: "Tea" },  { id: 8, name: "Cake" },]; function findProduct(query: string | number) {  if (typeof query === "number") {    return products.find((product) => product.id === query);  }  return products.find((product) => product.name.toLowerCase() === query.toLowerCase());} console.log(findProduct(8)?.name);console.log(findProduct("tea")?.name);

Lines 1 to 4 make two products. Line 6 says query is text or a number. Line 7 checks for a number; inside, line 8 compares it with each product's id. Line 10 runs only for text, so query.toLowerCase() is allowed there, and it compares names without caring about capital letters. Line 13 finds product 8 and prints Cake. Line 14 finds "tea" and prints Tea. The ?. reads name only if a product was found (Nullish and optional chaining covers it).

Where does the union come from? A text box always gives text. The page can turn digits into a number and keep anything else as text, and TypeScript works out the union by itself:

A search box value that becomes text or a numberPop out in the code editor (opens in a new tab)TypeScript
const searchBox = document.createElement("input");searchBox.value = "8"; const typed = searchBox.value;const query = Number.isNaN(Number(typed)) ? typed : Number(typed);//    ^?console.log(query);

Lines 1 and 2 make a text box and put "8" in it, as if someone typed 8. (The Form elements lesson covers inputs.) Line 4 reads the text. Line 5 asks whether Number(typed) is NaN ("not a number"). If it is, query keeps the text; if not, it gets the number. The query on line 6 shows const query: string | number, because either could happen. Line 7 prints 8, and that query is ready to pass to findProduct.

Error or fine?
  • let id: string | number = "A-42";
  • let id: string | number = false;
  • function show(id: string | number) { return id.toString(); }
  • function show(id: string | number) { return id.length; }
  • function show(id: string | number) { return id + 1; }
  • function show(id: string | number) { return typeof id === "string" ? id.toUpperCase() : id; }
  • const sizes: (string | number)[] = [8, "M"];
  • const sizes: string[] | number[] = [8, "M"];
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

The order of a union's members doesn't matter. TypeScript may even show them in a different order from yours:

number | string, shown as string | numberPop out in the code editor (opens in a new tab)TypeScript
let price: number | string = 4;//  ^?

Line 1 says number | string. The query on line 2 shows let price: string | number. It's the same type: both say "text or a number".

  • "string | number means the value is both." No. It's one or the other at any moment. The | reads "or".
  • "If a union includes string, I can use string methods." No. You can use only what every member has, until a check like typeof orderId === "string" narrows it.
  • "A union converts my value." No. Types are removed before the code runs. formatPrice still needs Number(price) to turn text into a number.
  • "string | number[] is a list of text or numbers." No. That's one piece of text, or a list of numbers. A list of either is (string | number)[], with brackets.
  • "With a union of objects, I can read any property from either one." No. Only properties every object has, like status. Check with in before reading the rest.
  • "number | string and string | number are different types." No. The order doesn't matter, as the query above shows.

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 value that doesn't fit

A discount can be a number or text like "10%". Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
let discount: string | number = 10;
discount = "10%";
discount = false;
discount = 5;

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

    Exercise 2 · Warm-upRead the type of a mixed list

    No types are written here. What type does TypeScript give sizes? Guess first, then point at sizes or add a // ^? line under it.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const sizes = ["S", 10, "L"];

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

      Exercise 3 · PracticeLet a product ID be text too

      A product ID can be a number or text like "tea-7". Fix line 1 so the error on line 3 goes away.

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      let productId: number = 7;console.log(productId);productId = "tea-7";console.log(productId);

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

        Exercise 4 · PracticeCheck before you shout

        shout takes a label as text or a number. Fix line 2 so it works for both. What does shout(7) print then?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        function shout(label: string | number) {  return label.toUpperCase();} console.log(shout("sale"));console.log(shout(7));

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

          Exercise 5 · ChallengeRead a property only cards have

          A payment is a card or cash. TypeScript marks line 5. What does the starter's second call print when it runs? Then fix the function so each payment gets the right sentence.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Card = { method: string; last4: string };type Cash = { method: string; amount: number }; function describe(payment: Card | Cash) {  return `Paid by ${payment.method}, card ending ${payment.last4}`;} console.log(describe({ method: "card", last4: "4242" }));console.log(describe({ method: "cash", amount: 20 }));

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

            Exercise 6 · ChallengeCount items from a button or a text box

            A plus button passes a number, and a text box passes text. Fix addToCart so both work. What does your fixed code print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            let cartCount = 0; function addToCart(quantity: number) {  cartCount += quantity;} addToCart(2);addToCart("3");console.log(cartCount);

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

              Check your understanding

              For each question, ask which member types a value could have, and whether the code works for every one of them. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The union types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the type string | number mean?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which line does TypeScript flag in these setSize calls?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function setSize(size: string | number) {
                  console.log(size);
                }
                
                setSize(12);
                setSize("large");
                setSize(["large"]);

                Choose an answer to see the explanation.

              3. Question 3 of 7What does TypeScript say about size.toFixed(1) here?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function showSize(size: string | number) {
                  return size.toFixed(1);
                }

                Choose an answer to see the explanation.

              4. Question 4 of 7In this showSize, what type does size have on line 3?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function showSize(size: string | number) {
                  if (typeof size === "number") {
                    return size.toFixed(1);
                  }
                  return size;
                }

                Choose an answer to see the explanation.

              5. Question 5 of 7Which type is a list where each item can be text or a number?

                Choose an answer to see the explanation.

              6. Question 6 of 7This code has a type error. What does addOne("4") print when it runs anyway?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function addOne(price: string | number) {
                  return price + 1;
                }
                
                console.log(addOne("4"));

                Choose an answer to see the explanation.

              7. Question 7 of 7Which property can logReply read from reply without a check first?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Found = { status: number; results: string[] };
                type Failed = { status: number; message: string };
                
                function logReply(reply: Found | Failed) {
                  console.log(reply);
                }

                Choose an answer to see the explanation.

              Key takeaways

              • A union type says a value can be one of several types: string | number is text or a number.
              • Any value of a member type fits. Anything else, like true or null, gets an error.
              • You can use only what every member can do, like toString(), until you check.
              • A typeof check, or in for objects, narrows the union to one member inside the check.
              • Union parameters let one function accept several kinds of input; you still convert the value yourself.
              • TypeScript works out unions too: number | undefined from find, (string | number)[] for a mixed list.

              Remember the one-liner.
              A union means "one of these": store any of them, use what they share, and check before using the rest.

              Coming next: Literal types. A type can be one exact value, like "small", and a union of those makes a list of allowed options.

              CompleteFrontend Clear concepts. Working examples.