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.
- 01Write 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.
- 02Know which assertions are allowedPredict when TypeScript accepts
as, when it says the types don't overlap, and whatas unknown asreally does. - 03Choose an annotation, an assertion, or a checkUse an annotation for values you create,
asonly 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:
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 avalue". 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.
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.
#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.
const searchBox = document.querySelector("#search") as HTMLInputElement;console.log(searchBox.value);HTMLInputElementNo errorsprints teaAn HTMLInputElement is an <input> box, and every input box has a value. TypeScript accepts it, and the code prints tea.
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.
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
#searchis an input box - In real life: You write "Kitchen: plates" on the box
- In TypeScript: You write
as HTMLInputElementafter 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
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.
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:
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.
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:
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 | nulltoHTMLInputElement: a more specific type. An input box is one of the thingsElement | nullcan hold, so it's allowed. - Line 3 goes from
HTMLInputElementtoElement: 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.
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.
| The assertion | Allowed? | Why |
|---|---|---|
found as HTMLInputElement | Yes. | Element | null can hold an input box, so this is a more specific type. |
input as Element | Yes. | Every input box is an element, so this is a less specific type. |
input as unknown | Yes. | unknown can hold any value, so every type fits it. |
input as number | No: 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:
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.
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.
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:
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:
type CartItem = { name: string; price: number }; const tea: CartItem = { name: "Tea", price: 3 };console.log(tea.price * 2);6an annotationNo errorsLine 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.
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.
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.
| Question | Annotation: 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 price | An error: Property 'price' is missing. | No error. The price is still missing when the code runs. |
| The types have nothing in common | An 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).
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):
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.
| The situation | What to write | Why |
|---|---|---|
| An element your own HTML always has, found by its id | as HTMLInputElement is fine. | You wrote the page, so you know what the id points at. |
event.target in a listener on one input box | as HTMLInputElement is fine. | The listener is on that box, so the target is that box. |
A cart you saved yourself, read back with JSON.parse | as 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 typed | Check 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 number | Convert it with Number(). | as number doesn't change text into a number, and TypeScript refuses it anyway. |
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 } = {};
Sort each line by what TypeScript says about it. Each line is checked on its own.
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:
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 numberturns a value into a number." No. An assertion never changes a value. UseNumber()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
HTMLSelectElementfor 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 givesHTMLInputElement | null. You needasfor 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.
Which line will TypeScript mark?
const priceText = "12";
const price = priceText as number;
console.log(price + 1);const priceText = "12";const price = Number(priceText);console.log(price + 1);Line 2 asserts that the text "12" is a number. The two types don't overlap, so TypeScript reports error 2352 there. The fix converts instead: Number(priceText) is the number 12, and the code prints 13.
What type does TypeScript give user? Guess first. Then point at user to check, or press Edit & run and add a // ^? line under it.
type User = { name: string; age: number }; const reply = '{ "name": "Asha", "age": 30 }';const user = JSON.parse(reply) as User;type User = { name: string; age: number }; const reply = '{ "name": "Asha", "age": 30 }';const user = JSON.parse(reply) as User;// ^?as User tells TypeScript the parsed value is a User, so the query on line 5 shows const user: User. Without the assertion it would be any.
TypeScript marks line 2 twice. Fix line 1 so the code reads the email box. What type do you write?
const emailBox = document.querySelector("#email");console.log(emailBox.value);const emailBox = document.querySelector("#email") as HTMLInputElement;console.log(emailBox.value);as HTMLInputElement tells TypeScript the element is an input box, so both errors on line 2 are gone. The code prints asha@example.com.
TypeScript reports nothing here. What does the code print? Then change line 3 to an annotation, read the error, and fix the cake.
type CartItem = { name: string; price: number }; const cake = { name: "Cake" } as CartItem;console.log(cake.price * 2);type CartItem = { name: string; price: number }; const cake: CartItem = { name: "Cake", price: 4 };console.log(cake.price * 2);The starter prints NaN, because the assertion let the missing price through. With the annotation const cake: CartItem, TypeScript reports Property 'price' is missing. Adding price: 4 fixes it, and the code prints 8.
A saved cart with a tea and a cake. TypeScript is happy. What does it print? Then fix it so it counts the items.
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`);type CartItem = { name: string; price: number }; const saved = '[{"name":"Tea","price":3},{"name":"Cake","price":4}]';const cart = JSON.parse(saved) as CartItem[];console.log(`${cart.length} items`);The starter prints 52 items: the saved text is 52 characters long. JSON.parse(saved) makes a real list of two items, as CartItem[] gives it its type, and the code prints 2 items.
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.
const searchBox = document.querySelector("#serch") as HTMLInputElement;console.log(searchBox.value.toUpperCase());const searchBox = document.querySelector("#serch");if (searchBox instanceof HTMLInputElement) { console.log(searchBox.value.toUpperCase());} else { console.log("No search box");}#serch is misspelled, so querySelector gives back null, and reading value from null throws a TypeError. The assertion hid that from TypeScript. With instanceof, the check happens while the page runs, and the code prints No search box.
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.
Question 1 of 7What does
as HTMLInputElementdo when the code runs?Choose an answer to see the explanation.
Question 2 of 7What does this double assertion print?
Read the code, then predictconst price = "4" as unknown as number; console.log(price + 1);Choose an answer to see the explanation.
Question 3 of 7Which line of this cart does TypeScript flag?
Read the code, then predicttype CartItem = { name: string; price: number }; const tea = { name: "Tea" } as CartItem; const cake: CartItem = { name: "Cake" };Choose an answer to see the explanation.
Question 4 of 7Why does TypeScript reject
"4" as number?Choose an answer to see the explanation.
Question 5 of 7What happens when this code runs on a page with no #missing element?
Read the code, then predictconst searchBox = document.querySelector("#missing") as HTMLInputElement; console.log(searchBox.value);Choose an answer to see the explanation.
Question 6 of 7What type does TypeScript give
settingson line 2?Read the code, then predictconst reply = '{ "theme": "dark" }'; const settings = JSON.parse(reply);Choose an answer to see the explanation.
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.
asis allowed only when the types overlap: a more specific or less specific type. Otherwise you get error 2352.as unknown asgets 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 withNumber().
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.