cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Type assertions

Use as to tell TypeScript what you know about a value, see which assertions it allows, and spot when one hides a bug, with a search box you can edit.

By the end, you can
  • 01
    Write an assertion with asTell TypeScript a more exact type for a value, like the input box a selector finds, and know that nothing checks it when the code runs.
  • 02
    Know which assertions are allowedPredict when TypeScript accepts as, when it says the types don't overlap, and what as unknown as really does.
  • 03
    Choose an annotation, an assertion, or a checkUse an annotation for values you create, as only for facts TypeScript can't see, and a real check when you aren't sure.

Telling TypeScript what you know

A shop page has a search box: <input id="search">, with tea typed in it. Your code finds the box with document.querySelector, which gives back the first element that matches a CSS selector (the DOM navigation lesson covers it). Then it reads what was typed:

Read a search box by its id (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const searchBox = document.querySelector("#search");//    ^?console.log(searchBox.value);

Line 1 asks the page for the element with the id search. The query on line 2 shows what TypeScript knows about it: const searchBox: Element | null. An Element is any element on a page, like a <div> or an <input>, and null means nothing was found. Line 3 reads value, and TypeScript marks it twice, on purpose here:

  • 'searchBox' is possibly 'null' means "the page might have no element with this id".
  • Property 'value' does not exist on type 'Element' means "not every element has a value". A <div> doesn't, so TypeScript won't let you read it from any element.

But you wrote the page, so you know #search is an input box, and input boxes always have a value. TypeScript can't see your HTML. It only reads the code. A type assertion passes on what you know: you write as and a type after a value.

Tell TypeScript the element is an input boxPop out in the code editor (opens in a new tab)TypeScript
const searchBox = document.querySelector("#search") as HTMLInputElement;//    ^?console.log(searchBox.value);

Line 1 now ends with as HTMLInputElement. HTMLInputElement is the browser's type for an <input> box. The query on line 2 shows TypeScript believes you: const searchBox: HTMLInputElement. Both errors are gone, and line 3 prints tea.

Left, a page window holds the element input id="search": you know it is an input box. Right, an editor window shows the type Element | null: all TypeScript knows. An arrow labelled as goes from the page to the editor.What you knowpage<input id="search">an input boxasyou tell itWhat TypeScript knowseditorElement | nullsome element, or nothing
You can see the page, so you know #search is an input box. TypeScript reads only the code, so all it knows is "some element, or nothing". as HTMLInputElement passes on what you know.

Try it: type a different type after as, and watch the list under the code as you type.

Playground: type what comes after as
Read the search boxPop out in the code editor (opens in a new tab)TypeScript
const searchBox = document.querySelector("#search") as HTMLInputElement;console.log(searchBox.value);
You told TypeScriptHTMLInputElement
What happens
TypeScriptNo errors
When it runsprints tea
Try it yourself

An HTMLInputElement is an <input> box, and every input box has a value. TypeScript accepts it, and the code prints tea.

The code is checked by the real TypeScript compiler each time you type. Only the type after as changes. Reset puts HTMLInputElement back.

HTMLElement is any element, so line 2 still can't read value. HTMLSelectElement, a drop-down list, has a value too, so TypeScript accepts it even though the page holds an input box. And number is refused outright, because an element can never be a number. Whatever you type, the code prints tea when it runs: the assertion only changes what TypeScript believes.

Real-life analogyA label on a moving box

When you move house, you pack a box and write "Kitchen: plates" on it. The movers can't see inside, so they trust your label and carry the box to the kitchen. If you wrote the wrong label, nobody finds out until the box is opened in the wrong room.

In real life: You pack a box, so you know what's inside
In TypeScript: You wrote the page, so you know #search is an input box
In real life: You write "Kitchen: plates" on the box
In TypeScript: You write as HTMLInputElement after the value
In real life: The movers trust the label and don't open the box
In TypeScript: TypeScript trusts the assertion and doesn't check it
In real life: The label doesn't change what's inside
In TypeScript: The value is the same when the code runs
In real life: A wrong label is found when someone unpacks
In TypeScript: A wrong assertion shows up when the code runs
The main idea

A type assertion tells TypeScript what type a value has, when you know more than TypeScript can see. You write it with as, like querySelector("#search") as HTMLInputElement. TypeScript trusts it and doesn't check it, and it's removed before the code runs.

Writing an assertion with as

An assertion goes after a value: the value, the word as, then a type. You'll use it most for values that TypeScript knows little about. JSON.parse is one: it turns text into an object (the JSON lesson covers it), and TypeScript can't read what's inside the text.

Give a parsed reply a typePop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; age: number }; const reply = '{ "name": "Asha", "age": 30 }';const data = JSON.parse(reply);//    ^?const user = JSON.parse(reply) as User;//    ^?console.log(user.name);

Line 1 describes a User: a name that is text and an age that is a number. Line 3 is a reply from a server, as text. Line 4 parses it, and the query on line 5 shows const data: any. any means "TypeScript stops checking this value": you could misspell data.nmae and get no error. Line 6 parses the same text and adds as User, so the query on line 7 shows const user: User. Now TypeScript checks every use of user, and line 8 prints Asha.

Like every type, an assertion is removed before the browser runs the code. Here is what the browser gets from the overview's example:

What the browser gets: the assertion is gonePop out in the code editor (opens in a new tab)JavaScript
const searchBox = document.querySelector("#search");
console.log(searchBox.value);

Line 1 has lost as HTMLInputElement. Nothing is left to check that the element really is an input box, so an assertion can never stop a mistake while the page runs. It only changes what TypeScript believes while it checks your code.

The names are close, so keep two things apart. An assertion function, like assertIsDefined in the Type predicates & assertion functions lesson, is real code: it checks the value while the page runs and throws when the check fails. A type assertion with as has no code at all.

There is a second way to write an assertion: the type in angle brackets, before the value.

The same assertion in angle bracketsPop out in the code editor (opens in a new tab)TypeScript
const searchBox = <HTMLInputElement>document.querySelector("#search");//    ^?console.log(searchBox.value);

Line 1 writes <HTMLInputElement> before the value. The query on line 2 shows the same type, and line 3 prints tea. This form doesn't work in .tsx files, the files that hold JSX for React, because <HTMLInputElement> looks like a JSX tag there. Most code uses as, and so does this lesson.

You've met one assertion already. In the null and undefined lesson, a ! after a value told TypeScript "this isn't null". That ! is a type assertion too, for that one fact. as can say any type.

Which assertions TypeScript allows

TypeScript trusts an assertion, but not blindly. It allows as only when the two types overlap: when one of them fits inside the other. Here are four assertions in a row:

Three assertions TypeScript allows and one it refuses (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const found = document.querySelector("#search");const input = found as HTMLInputElement; // more specificconst element = input as Element; // less specificconst anything = input as unknown; // fits every typeconst count = input as number; // nothing in common
  • Line 2 goes from Element | null to HTMLInputElement: a more specific type. An input box is one of the things Element | null can hold, so it's allowed.
  • Line 3 goes from HTMLInputElement to Element: a less specific type. Every input box is an element, so it's allowed. You rarely need this one; TypeScript would accept the input box as an element anyway.
  • Line 4 goes to unknown, a type that can hold any value at all. Every type fits inside it, so it's allowed.
  • Line 5 goes from an input box to number. No input box is a number, and no number is an input box, so TypeScript marks it on purpose.

The message on line 5 is long: Conversion of type 'HTMLInputElement' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. In plain words: "these two types have nothing in common, so this is probably a mistake". Its number is ts(2352). The last sentence hints at a way around it, which the next section looks at.

A column of three cards: unknown at the top, Element | null in the middle, HTMLInputElement at the bottom. An arrow down is labelled more specific and an arrow up is labelled less specific. Past a dashed line, a card number stands alone, marked no overlap, error 2352.AllowedunknownElement | nullHTMLInputElementdown: more specificup: less specificNot allowednumberno overlaperror 2352
as can move down to a more specific type or up to a less specific one. unknown sits at the top, above every type. number isn't on this line at all, so input as number is error 2352.

The table below sums up the four lines. Read each row as: this assertion, does TypeScript allow it, and why.

Which assertions TypeScript allows
The assertionAllowed?Why
found as HTMLInputElementYes.Element | null can hold an input box, so this is a more specific type.
input as ElementYes.Every input box is an element, so this is a less specific type.
input as unknownYes.unknown can hold any value, so every type fits it.
input as numberNo: error 2352.No input box is a number, and no number is an input box. The two types have nothing in common.

Double assertions: as unknown as

A shop saves the cart in the browser's storage, which keeps only text (the Web storage lesson covers it). Reading it back gives you a string. Here someone tries to treat that string as a list of cart items:

Saved text treated as a cart (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number }; const saved = '[{"name":"Tea","price":3}]';const cart = saved as CartItem[];

Line 1 describes a CartItem. Line 3 is the saved cart: text that describes one tea. Line 4 says it's a CartItem[], a list of cart items. TypeScript refuses, on purpose here, with the message from the last section: Conversion of type 'string' to type 'CartItem[]' may be a mistake. Text is never a list.

The message said "convert the expression to 'unknown' first". Writing two assertions in a row, first to unknown and then to the type you want, is a double assertion. Each step is allowed on its own: any type goes up to unknown, and unknown goes down to any type.

The same text through unknownPop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number }; const saved = '[{"name":"Tea","price":3}]';const cart = saved as unknown as CartItem[];//    ^?console.log(cart.length);

Line 4 now says as unknown as CartItem[]. There is no error, and the query on line 5 shows const cart: CartItem[]. But line 6 prints 26, not 1. The value is still the text from line 3, and a string's length counts its characters.

A window cart.ts holds const cart = saved as unknown as CartItem[]. An arrow labelled types removed leads to a console window where cart.length gives 26.cart.tsconst cart = savedas unknownas CartItem[];TypeScript: a list of itemstypes removedconsolecart.length2626 characters, not 1 item
Both assertions are removed with the types. cart is still the text in saved, so cart.length counts its 26 characters instead of 1 item.

TypeScript's error was right: the code had a bug, and the double assertion only hid it. The fix is to turn the text into a real list with JSON.parse:

Parse the saved text, then assert its typePop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number }; const saved = '[{"name":"Tea","price":3}]';const cart = JSON.parse(saved) as CartItem[];//    ^?console.log(cart.length);

Line 4 parses the text, which gives back any, and asserts it's a CartItem[]. The query on line 5 shows the same type, and line 6 prints 1: one tea. This single assertion is fine, because you saved this cart yourself and know its shape.

You'll also see as any as, which does the same thing through any. A double assertion turns off the check TypeScript has left. Keep it for the rare case where you really know better, such as a fake object in a test. When TypeScript says two types don't overlap, first look for the bug.

Assertions vs annotations

An annotation is the colon and type after a name, like const tea: CartItem. It looks a little like an assertion, and both give tea the type CartItem. But they ask TypeScript different questions. An annotation asks "does this value fit the type?" An assertion only asks "could this value be the type?" Press each button and watch the list under the code:

Playground: annotation or assertion
One tea for the cartPop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number }; const tea: CartItem = { name: "Tea", price: 3 };console.log(tea.price * 2);
Console output6
What happens
Line 3 usesan annotation
TypeScriptNo errors
Try it yourself

Line 3 says tea is a CartItem with a colon: an annotation. TypeScript checks the object against the type. It has a name and a number price, so there are no errors, and the code prints 6.

TypeScript checks the code for real, and the output is what this code prints. Each button changes only line 3. Reset goes back to Annotation, full item.

Annotation, full item has no errors and prints 6. Annotation, price left out gets Property 'price' is missing: the annotation checked the object and found no price. Assertion, price left out is the same object with as CartItem, and TypeScript says nothing. A CartItem has a name, so the two types overlap, and the assertion is allowed. The price is still missing, so tea.price * 2 prints NaN, which means "not a number". Assertion, price as text gets error 2352, because a text price can never be a number price.

Left, annotation: an object card name: "Tea" goes into a box labelled tea, and the check finds the missing price. Right, assertion: the same card has a tag that says as CartItem stuck to it, and nothing looks inside, so the price stays missing.Annotationname: "Tea"teachecked: price is missingAssertionname: "Tea"asCartItemtrusted: price stays missing
The annotation const tea: CartItem checks the object, so the missing price is an error. The assertion as CartItem only sticks a label on it. Nothing looks inside, and tea.price * 2 prints NaN.

So when you make a value yourself, use an annotation. It checks every property, so a missing one is caught before the code runs. Keep as for facts TypeScript can't see. The table compares the two. Read each row as a question, then how each one answers it.

Annotations and assertions, side by side
QuestionAnnotation: const tea: CartItem = …Assertion: … as CartItem
What does it say?This value must fit the type. Check it.Treat this value as the type. Trust me.
A property is missing, like the priceAn error: Property 'price' is missing.No error. The price is still missing when the code runs.
The types have nothing in commonAn error.An error too: error 2352, unless you go through unknown.
What happens when the code runs?Nothing. It's removed with the types.Nothing. It's removed with the types too.
When should you use it?Most of the time, for any value you create yourself.Only for a fact TypeScript can't see, like what your page's HTML holds.

A live search box

A real search box shows results as the shopper types. The code listens for the box's input event, which fires on every key press (the Introduction to events lesson covers listeners).

Search as the shopper typesPop out in the code editor (opens in a new tab)TypeScript
const searchBox = document.querySelector("#search") as HTMLInputElement; searchBox.addEventListener("input", (event) => {  const box = event.target as HTMLInputElement;  //                ^?  console.log(`Searching for ${box.value}`);}); searchBox.value = "cake";searchBox.dispatchEvent(new Event("input"));

Line 1 finds the box and asserts it's an input box, as in the overview. Line 3 adds a listener for the input event. Inside it, line 4 reads event.target, the element the event happened on. The query on line 5 shows its type, EventTarget | null: TypeScript only knows it's something that can get events. You put the listener on the search box, so you know the target is that box, and as HTMLInputElement says so. Lines 9 and 10 act like a shopper: they type cake and fire the event, so line 6 prints Searching for cake.

Both assertions are safe because you know your own page. When you aren't sure, check instead, with a check in an if or a type guard. instanceof asks while the page runs whether a value was made by a class, such as HTMLInputElement (the Checking types reliably lesson covers it):

Check the element instead of assertingPop out in the code editor (opens in a new tab)TypeScript
const found = document.querySelector("#search"); if (found instanceof HTMLInputElement) {  console.log(`Searching for ${found.value}`);  //                             ^?} else {  console.log("No search box on this page");}

Line 1 finds the element with no assertion. Line 3 checks it. Inside the if, TypeScript has narrowed found to an input box (the Narrowing lesson explains how): the query on line 5 shows const found: HTMLInputElement, so reading found.value has no error. Line 4 prints Searching for tea. On a page with no search box, line 7 would print a message instead of throwing an error.

The table lists common places you might reach for as. Read each row as: this situation, what to write, and why.

When as is fine, and when to check instead
The situationWhat to writeWhy
An element your own HTML always has, found by its idas HTMLInputElement is fine.You wrote the page, so you know what the id points at.
event.target in a listener on one input boxas HTMLInputElement is fine.The listener is on that box, so the target is that box.
A cart you saved yourself, read back with JSON.parseas CartItem[] is common.You know the shape you saved. Check it if old saved data could have another shape.
A reply from a server, or anything a user typedCheck it instead.It may not match what you expect. Code like typeof, a type guard such as isUser, or Number() checks or converts it.
Text that should be a numberConvert it with Number().as number doesn't change text into a number, and TypeScript refuses it anyway.
Error or fine?
  • const price = "4" as number;
  • const price = "4" as unknown as number;
  • const box = document.querySelector("#search") as HTMLInputElement;
  • const box = document.querySelector("#search") as string;
  • const item = {} as { name: string; price: number };
  • const item = { name: "Tea", price: "3" } as { name: string; price: number };
  • const input = document.createElement("input") as HTMLElement;
  • const item: { name: string; price: number } = {};
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 most common mix-up is to use as to change a value. Here is text asserted as a number, next to a real conversion:

An assertion next to a conversionPop out in the code editor (opens in a new tab)TypeScript
const quantity = "3" as unknown as number;//    ^?console.log(quantity + 1);console.log(Number("3") + 1);

Line 1 asserts the text "3" is a number, and the query on line 2 shows TypeScript believes it. Line 3 adds 1 and prints 31, because the value is still text and + joins text. Line 4 converts with Number("3"), which really makes the number 3, and prints 4.

  • "as number turns a value into a number." No. An assertion never changes a value. Use Number() to convert text.
  • "TypeScript checks my assertion while the page runs." No. The assertion is removed with the types. A wrong one shows up later, as a wrong value or an error.
  • "An assertion and an annotation do the same thing." No. An annotation checks that the value fits. An assertion only checks that the types overlap, so a missing property gets through.
  • "No error means my assertion is right." No. TypeScript allowed HTMLSelectElement for an input box. It can't see the page, so it can't know.
  • "When TypeScript refuses an assertion, add as unknown." No. Error 2352 usually means a real bug, like the saved cart that was still text.
  • "Every element needs an assertion." No. querySelector("input") with a tag name already gives HTMLInputElement | null. You need as for selectors like ids and classes.

Practice exercises

Each exercise is TypeScript. Press Edit & run on the starter code to try a fix: the errors update as you type, and the console shows what it prints. The page for these exercises has a search box holding tea and an email box holding asha@example.com.

Exercise 1 · Warm-upFind the assertion TypeScript refuses

Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
const priceText = "12";
const price = priceText as number;
console.log(price + 1);

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

    Exercise 2 · Warm-upRead an asserted type

    What type does TypeScript give user? Guess first. Then point at user 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
    type User = { name: string; age: number }; const reply = '{ "name": "Asha", "age": 30 }';const user = JSON.parse(reply) as User;

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

      Exercise 3 · PracticeRead the email box

      TypeScript marks line 2 twice. Fix line 1 so the code reads the email box. What type do you write?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      const emailBox = document.querySelector("#email");console.log(emailBox.value);

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

        Exercise 4 · PracticeSwap an assertion for an annotation

        TypeScript reports nothing here. What does the code print? Then change line 3 to an annotation, read the error, and fix the cake.

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type CartItem = { name: string; price: number }; const cake = { name: "Cake" } as CartItem;console.log(cake.price * 2);

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

          Exercise 5 · ChallengeFind what a double assertion hides

          A saved cart with a tea and a cake. TypeScript is happy. What does it print? Then fix it so it counts the items.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type CartItem = { name: string; price: number }; const saved = '[{"name":"Tea","price":3},{"name":"Cake","price":4}]';const cart = saved as unknown as CartItem[];console.log(`${cart.length} items`);

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

            Exercise 6 · ChallengeCheck instead of asserting

            TypeScript reports nothing, but this code fails when it runs. What kind of error does it throw? Then replace the assertion with a check, so a missing box prints No search box.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            const searchBox = document.querySelector("#serch") as HTMLInputElement;console.log(searchBox.value.toUpperCase());

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

              Check your understanding

              For each question, keep two moments apart: what TypeScript believes while it checks, and what the value really is when the code runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The type assertions quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does as HTMLInputElement do when the code runs?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this double assertion print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const price = "4" as unknown as number;
                console.log(price + 1);

                Choose an answer to see the explanation.

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

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type CartItem = { name: string; price: number };
                
                const tea = { name: "Tea" } as CartItem;
                const cake: CartItem = { name: "Cake" };

                Choose an answer to see the explanation.

              4. Question 4 of 7Why does TypeScript reject "4" as number?

                Choose an answer to see the explanation.

              5. Question 5 of 7What happens when this code runs on a page with no #missing element?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const searchBox = document.querySelector("#missing") as HTMLInputElement;
                console.log(searchBox.value);

                Choose an answer to see the explanation.

              6. Question 6 of 7What type does TypeScript give settings on line 2?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const reply = '{ "theme": "dark" }';
                const settings = JSON.parse(reply);

                Choose an answer to see the explanation.

              7. Question 7 of 7A form sends you a price as text. What should you write?

                Choose an answer to see the explanation.

              Key takeaways

              • A type assertion, value as Type, tells TypeScript what type a value has when you know more than it can see, like which element a selector finds.
              • TypeScript trusts an assertion. It's removed before the code runs, so nothing checks it, and it never changes a value.
              • as is allowed only when the types overlap: a more specific or less specific type. Otherwise you get error 2352.
              • as unknown as gets past that error, but the value stays what it was. Error 2352 usually points at a real bug.
              • An annotation checks that a value fits; an assertion doesn't. Use annotations for values you create.
              • When you aren't sure, check the value with code like instanceof, or convert it with Number().

              Remember the one-liner.
              as changes what TypeScript believes, never what the value is.

              Coming next: as const & satisfies. Two more ways to talk to TypeScript about a value: keep its exact values, and check it against a type without losing them.

              CompleteFrontend Clear concepts. Working examples.