Types in JavaScript files
Check plain .js files with TypeScript: turn checking on with // @ts-check or checkJs, write types in JSDoc comments, and hide an error the safe way.
- 01Turn checking onCheck one JavaScript file with
// @ts-check, or every one withcheckJs, and say whatallowJsadds. - 02Write types in JSDocGive parameters, return values, variables, and object shapes their types with
@param,@returns,@type, and@typedef. - 03Hide an error safelyChoose between
@ts-expect-error,@ts-ignore, and@ts-nocheck, and know what each one hides.
A JavaScript file that checks itself
A checkout page adds 5 to a price for shipping. The price comes from a form, and a form gives you text. The project is plain JavaScript: no .ts files and no build step. TypeScript can still check it:
// @ts-check /** @param {number} price */function addShipping(price) { return price + 5;} const priceFromForm = "20";console.log(addShipping(priceFromForm));Line 1, // @ts-check, is a comment that asks TypeScript to check this file. Line 3 is a JSDoc comment: a comment that starts with /**. Its @param {number} price says the parameter price must be a number. Lines 4 to 6 add 5 to it. Line 8 holds the price as text, the way a form gives it. Line 9 passes that text to addShipping, and TypeScript marks it, on purpose here:
Argument of type 'string' is not assignable to parameter of type 'number'means "you're passing text to a parameter that has to be a number". An argument is the value you pass in a call.ts(2345)is the error's number, the same as in a.tsfile.
The file is still JavaScript. The browser sees two comments, skips them, and runs the file as it is. "20" + 5 joins the two (the Type conversion lesson explains why), so line 9 prints 205. The check happens only in your editor, and in tsc when you run it.
shipping.js, which prints 205. Nothing is built in between.You write pencil notes in the margin of a family recipe book: "2 eggs, not 3". The cook follows the printed recipe and doesn't need the notes. A friend who checks the recipe reads them and spots where the recipe and the notes disagree. Nobody has to reprint the book.
- In real life: Pencil notes in the margin of a recipe
- In TypeScript: JSDoc comments above your code
- In real life: The cook follows the recipe and skips the notes
- In TypeScript: The browser runs the file and skips the comments
- In real life: A friend reads the notes and spots "3 eggs, but the note says 2"
- In TypeScript: TypeScript reads the JSDoc and marks a value that breaks it
- In real life: You never have to reprint the book
- In TypeScript: No build step: the .js file stays as it is
You can type-check JavaScript without turning it into TypeScript. JSDoc comments, like /** @param {number} price */, hold the types. // @ts-check at the top of one file, or checkJs in tsconfig.json, turns the checking on. The browser skips the comments and runs the file unchanged.
If JSDoc is new to you, the JavaScript lesson Types for JavaScript introduces it. This lesson looks at it from TypeScript's side: what gets checked, which settings turn it on, and how it compares with a .ts file.
Turning on checking for one file
Editors like VS Code run TypeScript on JavaScript files too. They use it for autocomplete and for the types you see when you point at a name. But they don't report type errors in a .js file unless the file asks. The simplest way to ask is one comment on line 1. Try the three choices:
// Shipping for the checkout page /** @param {number} price */function addShipping(price) { return price + 5;} const priceFromForm = "20";console.log(addShipping(priceFromForm));// Shipping for the checkout pageNo errorsprints 205Line 1 is an ordinary comment. TypeScript still reads the JSDoc on line 3, but this file never asked to be checked, so nothing is marked. When it runs, it prints 205.
With A plain comment, nothing is marked, although the bug is still there. // @ts-check marks line 9. // @ts-nocheck turns reporting off for this file. Each time, the code prints 205: these comments change what TypeScript says, never what JavaScript does.
The comment only counts at the top of the file, before any code. Other comments and blank lines may come before it. Here it comes too late:
const shopName = "Tea Corner";// @ts-check /** @type {number} */const itemCount = "3";console.log(shopName, itemCount);Line 1 is code, so the // @ts-check on line 2 is only a comment. Line 5 breaks its own JSDoc, which says itemCount is a number, and nothing is marked. Line 6 prints Tea Corner 3. Press Edit & run and swap lines 1 and 2: line 5 gets Type 'string' is not assignable to type 'number'.
Once a file is checked, TypeScript works out types from values, the way it does in a .ts file. This is called inference (Annotations & inference covers it):
// @ts-check const cart = { items: 2, total: 7 };cart.items = "3";console.log(cart.totl);There's no JSDoc here. Line 3 makes a cart, and TypeScript works out that items and total are numbers. Line 4 stores text in items: Type 'string' is not assignable to type 'number'. Line 5 misspells total, and the message ends with the fix: Did you mean 'total'? Both errors are on purpose. When it runs, line 5 prints undefined.
Function parameters are different. A value tells TypeScript its type, but a parameter has no value until someone calls the function:
// @ts-check function addShipping(price) { return price + 5;} console.log(addShipping("20"));console.log(addShipping());Line 3's price has no type, so TypeScript gives it the type any, which means "anything goes, check nothing" (any, unknown & never covers it). This page checks with strict on, so TypeScript reports it: Parameter 'price' implicitly has an 'any' type. Without strict, you wouldn't even get that.
Line 7 passes text and line 8 passes nothing, and neither is marked. In a JavaScript file, a parameter with no JSDoc is also optional, because plain JavaScript lets you leave arguments out. They print 205 and NaN. A JSDoc type fixes all three problems, and that's the next section.
Writing types with @type and @param
A JSDoc comment starts with /** and sits right above what it describes. It has these parts:
- Tags that start with
@, like@param, which name what is being described. - A type in curly braces, like
{number}. Inside the braces you write a TypeScript type:number,string[], or"light" | "dark".
Here is a function with three tags:
// @ts-check /** * @param {number} price * @param {number} [percent=10] * @returns {string} */function salePrice(price, percent = 10) { return (price - (price * percent) / 100).toFixed(2);} const label = salePrice(20);// ^?console.log(label);Line 4 says price is a number. Line 5 says percent is a number too: the square brackets make it optional, and =10 notes its default. Line 6, @returns {string}, says what the function gives back; toFixed(2) on line 9 turns a number into text with two decimals. Line 12 calls salePrice(20), so percent is 10, and the query on line 13 shows const label: string. Line 14 prints 18.00.
Point at salePrice on line 8 to see the type TypeScript built from the comment: function salePrice(price: number, percent?: number): string. That's exactly what TypeScript syntax gives:
function salePrice(price: number, percent: number = 10): string { return (price - (price * percent) / 100).toFixed(2);} const label = salePrice(20);// ^?console.log(label);Line 1 holds the same three facts as the comment: a number price, a number percent that is 10 when left out, and a string result. The query on line 6 gives the same answer, const label: string, and line 7 prints 18.00.
salePrice, twice. In price.js the types sit in a JSDoc comment; in price.ts they sit in the code. Either way, TypeScript sees a price that is a number and a result that is a string.@type gives a variable its type. To give a name to an object shape, write @typedef with one @property per property. It does the job of a type alias (Type aliases & interfaces):
// @ts-check /** * @typedef {object} CartItem * @property {string} name * @property {number} price */ /** @type {CartItem[]} */const cart = [ { name: "Tea", price: 3 }, { name: "Cake", price: "4" },];console.log(cart.length);Lines 3 to 7 name the shape CartItem: a string name and a number price. Line 9 says cart is a list of them. Line 12 gives the cake the text "4", so TypeScript marks it, on purpose: Type 'string' is not assignable to type 'number'. Line 14 prints 2.
Last, the JSDoc way to say "trust me" about a value, the job as does in TypeScript (Type assertions). Write the @type comment right before the value, and put the value in brackets:
// @ts-check const couponBox = /** @type {HTMLInputElement} */ (document.querySelector("#coupon"));// ^?console.log(couponBox.value);Line 3 looks up the coupon box. On its own, document.querySelector("#coupon") gives Element | null, because TypeScript can't tell from an id what kind of element it will find. The comment and the brackets around the call say it's an HTMLInputElement, the browser's type for an <input>, and the query on line 4 shows TypeScript believes it. Line 5 prints tea10, the value in this lesson's coupon box. Leave out the brackets and the comment is ignored. Like as, nothing checks this while the page runs.
The table below puts each JSDoc form beside its TypeScript form. Read each row as: what you want to say, how a .js file says it, and how a .ts file says it.
| What you want to say | In a .js file, with JSDoc | In a .ts file |
|---|---|---|
| A parameter's type | @param {number} price | price: number |
| An optional parameter with a default | @param {number} [percent=10] | percent: number = 10 |
| What a function gives back | @returns {string} | ): string |
| A variable's type | /** @type {string[]} */ above the variable | const tags: string[] |
| A named object shape | @typedef {object} CartItem with one @property per property | type CartItem = { name: string; price: number } |
| "Trust me, this is an input box" | /** @type {HTMLInputElement} */ (value), with the brackets | value as HTMLInputElement |
checkJs and allowJs for a whole project
A // @ts-check at the top of every file gets tiring. A project can turn checking on in its tsconfig.json instead (Setting up a project introduced that file):
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": true,
"noEmit": true
},
"include": ["src"]
}Line 3, allowJs, lets .js files be part of the project. Line 4, checkJs, reports type errors in all of them, as if each began with // @ts-check. Line 5 turns on the strict checks, and line 6 says "only check, don't write any files". Line 8 includes everything in the src folder.
The two options do different jobs. Many projects have both kinds of file. Here a TypeScript file imports a JavaScript one, which has no // @ts-check. Pick a setting:
import { addShipping } from "./shipping.js"; const total = addShipping(20);// ^?console.log(total);console.log(addShipping("20"));/** @param {number} price */export function addShipping(price) { return price + 5;} /** @param {number} price */export function shippingLabel(price) { return "$" + price.toFixd(2);}Neither1 error: line 1No errorsconst total: anyWithout allowJs, shipping.js isn't part of the project. Line 1 of checkout.ts gets Could not find a declaration file for module './shipping.js', and the query on line 4 shows total is any, so line 6's text goes through unchecked.
With Neither, checkout.ts can't see inside shipping.js. TypeScript reports Could not find a declaration file for module './shipping.js' on line 1, and total is any, so the wrong call on line 6 goes through. allowJs brings shipping.js into the project: its JSDoc types reach checkout.ts, total is a number, and line 6 is marked. allowJs + checkJs also reports what's wrong inside shipping.js: line 8 misspells toFixed. Setting only checkJs gives the same result as setting both, because allowJs turns on by itself when checkJs is set.
allowJs lets checkout.ts read shipping.js, so total is a number. checkJs also reports the errors inside shipping.js, like the misspelled toFixd on line 8.A single file can still opt out. // @ts-nocheck at its top wins over checkJs:
// @ts-nocheck /** @type {number} */const itemCount = "3";console.log(itemCount);The list under the code shows this example sets checkJs: true. Line 4 breaks its @type, but line 1 turns reporting off for this file, so nothing is marked, and line 5 prints 3. That's handy for an old file you plan to fix later. Until you do, it isn't checked at all.
The table below lists every switch in this lesson. Read each row as: what you write, where it goes, and what it does.
| What you write | Where it goes | What it does |
|---|---|---|
// @ts-check | At the top of one .js file | Reports type errors in that file. |
// @ts-nocheck | At the top of one file | Reports nothing in that file, even when checkJs is on. |
"allowJs": true | In tsconfig.json | Lets .js files be part of the project, so TypeScript files can import them and use their JSDoc types. |
"checkJs": true | In tsconfig.json | Reports type errors in every .js file in the project, as if each started with // @ts-check. It turns allowJs on too. |
// @ts-expect-error | On the line above an error | Hides the next line's error, and is itself an error when that line has none. |
// @ts-ignore | On the line above an error | Hides the next line's errors, and stays quiet forever, even when there are none. |
@ts-expect-error and @ts-ignore in JavaScript
In Your first typed program you met two comments that hide one line's error. They work the same way in a checked JavaScript file. Say the old coupon form still sends prices as text, and you can't change it yet:
// @ts-check /** @param {number} price */function addShipping(price) { return price + 5;} // @ts-expect-error: the old coupon form still sends textconsole.log(addShipping("20"));Line 8 says "the next line has an error, and I know", with the reason after the colon. Line 9 still passes text, but its error is hidden, so the list is empty. It still prints 205: hiding an error never fixes it. Now someone fixes line 9:
// @ts-check /** @param {number} price */function addShipping(price) { return price + 5;} // @ts-expect-error: the old coupon form still sends textconsole.log(addShipping(20));Line 9 now passes the number 20 and prints 25. There's nothing left to hide, so TypeScript marks line 8 itself: Unused '@ts-expect-error' directive. A directive is a comment that gives TypeScript an instruction. The error reminds you to delete the comment.
// @ts-ignore in its place would stay quiet, and would also hide any new mistake that ever lands on line 9. That's why @ts-expect-error with a reason is the better choice, in JavaScript files as in TypeScript files. Both hide one line; // @ts-nocheck hides a whole file. Reach for them in this order: fix the code first, then @ts-expect-error with a reason, and the other two only for a short time.
A settings panel with no build step
Small sites, browser extensions, and scripts often ship their .js files exactly as written. JSDoc lets them keep that and still be checked. Here is a settings panel. The reader types a font size in a box, which holds 18 on this lesson's page:
// @ts-check /** * @typedef {object} Settings * @property {"light" | "dark"} theme * @property {number} fontSize */ /** @type {Settings} */const settings = { theme: "dark", fontSize: 16 }; const sizeBox = /** @type {HTMLInputElement} */ (document.querySelector("#font-size"));settings.fontSize = sizeBox.value;settings.theme = "blue";console.log(settings.fontSize + 2);Lines 3 to 7 name the Settings shape: a theme that is "light" or "dark" (Literal types covers types like these), and a number fontSize. Line 10 makes the settings. Line 12 reads the box with the JSDoc cast from earlier. Line 13 stores the box's value, which is always text, where a number goes. Line 14 picks a theme that doesn't exist. Both are marked on purpose. Run it, and line 15 prints 182, because "18" + 2 joins text.
// @ts-check /** * @typedef {object} Settings * @property {"light" | "dark"} theme * @property {number} fontSize */ /** @type {Settings} */const settings = { theme: "dark", fontSize: 16 }; const sizeBox = /** @type {HTMLInputElement} */ (document.querySelector("#font-size"));settings.fontSize = Number(sizeBox.value);settings.theme = "light";console.log(settings.fontSize + 2);Line 13 turns the text into a number with Number(…), and line 14 picks "light". No errors are left, and line 15 prints 20. The browser still runs this exact file, comments and all.
/** @type {number} */ let count = "3";/** @type {number} */ let count = Number("3");/** @type {"light" | "dark"} */ let theme = "blue";/** @param {string} name */ function greet(name) {} greet();function greet(name = "Asha") {} greet();let total: number = 7;const box = /** @type {HTMLInputElement} */ (document.querySelector("#coupon"));/** @type {string[]} */ const tags = [];
Sort each line by what TypeScript says about it in a checked .js file. Each line is checked on its own.
Common mix-ups
One mix-up is worth seeing in code. TypeScript's own syntax doesn't belong in a .js file:
// @ts-check let total: number = 7;console.log(total);Line 3 writes : number, which is TypeScript syntax. TypeScript says Type annotations can only be used in TypeScript files. The browser can't run the file at all: it stops with a SyntaxError before line 4. In a .js file, types go in JSDoc comments.
- "JSDoc comments change what my code does." No. They're comments, and the browser skips them.
addShipping("20")prints205with or without them. - "
// @ts-checkworks anywhere in the file." No. It has to come before any code. After a line of code, it's only a comment. - "
allowJschecks my JavaScript files." No. It lets them into the project, so TypeScript files can use their types.checkJsor// @ts-checkreports their errors. - "To get type checking, I have to rename my files to
.ts." No. A.jsfile with JSDoc and// @ts-checkis checked too, with no build step. - "A JSDoc cast checks the element." No.
/** @type {HTMLInputElement} */ (…)is trusted, likeas. If the element isn't an input box, nothing warns you. - "
@ts-ignoreand@ts-expect-errorare the same." No. Both hide the next line's error, but only@ts-expect-errortells you when there's nothing left to hide.
Practice exercises
Every exercise is a JavaScript file. When you fix code, press Edit & run on the starter code: TypeScript checks it as a .js file while you type, and the console shows what it prints. Try a guess first, then use the hints.
This file is checked. Which line will TypeScript mark?
// @ts-check
/** @type {{ name: string, price: number }[]} */
const cart = [
{ name: "Tea", price: 3 },
{ name: "Cake", price: "4" },
];// @ts-check /** @type {{ name: string, price: number }[]} */const cart = [ { name: "Tea", price: 3 }, { name: "Cake", price: "4" }, // line 6: the price is text];Line 6 gives the cake the text "4" where the JSDoc type says a number goes, so TypeScript reports Type 'string' is not assignable to type 'number' there.
What type does TypeScript give label? Guess first. Then point at label to check, or press Edit & run and add a // ^? line under it.
// @ts-check /** * @param {number} count * @returns {string} */function itemLabel(count) { return count === 1 ? "1 item" : `${count} items`;} const label = itemLabel(3);console.log(label);// @ts-check /** * @param {number} count * @returns {string} */function itemLabel(count) { return count === 1 ? "1 item" : `${count} items`;} const label = itemLabel(3);// ^?console.log(label);Line 5 says itemLabel returns a string, so label is a string. The query on line 12 shows const label: string, and line 13 prints 3 items.
This file has a bug and a JSDoc type that would catch it, but TypeScript reports nothing. Make TypeScript report the bug, then fix the call. What does your fixed code print?
/** @param {number} quantity */function addToCart(quantity) { return quantity + 1;} console.log(addToCart("2"));// @ts-check /** @param {number} quantity */function addToCart(quantity) { return quantity + 1;} console.log(addToCart(2));With // @ts-check on line 1, TypeScript marks addToCart("2"): Argument of type 'string' is not assignable to parameter of type 'number'. Passing 2 fixes it, and 2 + 1 prints 3 instead of 21.
Add a JSDoc comment so price is a number and the error goes away. What does the code print once the error is gone?
// @ts-check function formatPrice(price) { return "$" + price.toFixed(2);} console.log(formatPrice(4.5));// @ts-check /** @param {number} price */function formatPrice(price) { return "$" + price.toFixed(2);} console.log(formatPrice(4.5));The JSDoc on line 3 says price is a number, so it's no longer an implicit any and the error is gone. TypeScript now also checks that toFixed exists on a number. (4.5).toFixed(2) is "4.50", so it prints $4.50.
Line 9 was fixed last week, but the comment above it stayed. Predict what TypeScript says about line 8, then make the file clean. Which error number does TypeScript report on line 8?
// @ts-check
/** @param {number} price */
function addShipping(price) {
return price + 5;
}
// @ts-expect-error: the old coupon form still sends text
console.log(addShipping(20));// @ts-check /** @param {number} price */function addShipping(price) { return price + 5;} console.log(addShipping(20));Line 9 was fixed, so the comment on line 8 has nothing to hide, and TypeScript reports Unused '@ts-expect-error' directive, error 2578. Deleting the comment leaves a clean file that prints 25.
TypeScript marks line 4 twice. Fix both errors by changing only line 3, with a JSDoc comment. The page has a coupon box holding tea10. What does your fixed code print?
// @ts-check const couponBox = document.querySelector("#coupon");console.log(couponBox.value.toUpperCase());// @ts-check const couponBox = /** @type {HTMLInputElement} */ (document.querySelector("#coupon"));console.log(couponBox.value.toUpperCase());The JSDoc cast with brackets tells TypeScript the result is an HTMLInputElement, which has a value and isn't null, so both errors go. The box holds tea10, so it prints TEA10. Like as, the cast is trusted, not checked.
Check your understanding
For each question, keep two moments apart: what TypeScript says before the code runs, and what the browser does when it runs the file. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
// @ts-checkat the top of a .js file do?Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript mark in theme.js?
Read the code, then predict// @ts-check /** @type {"light" | "dark"} */ let theme = "dark"; theme = "light"; theme = "blue";Choose an answer to see the explanation.
Question 3 of 7This file has no
// @ts-check, and the project doesn't set checkJs. What does TypeScript report?Read the code, then predict/** @param {number} price */ function addShipping(price) { return price + 5; } console.log(addShipping("20"));Choose an answer to see the explanation.
Question 4 of 7What type does TypeScript give
boxon line 3?Read the code, then predict// @ts-check const box = /** @type {HTMLInputElement} */ document.querySelector("#coupon");Choose an answer to see the explanation.
Question 5 of 7Which tsconfig.json setting reports errors in every JavaScript file of a project?
Choose an answer to see the explanation.
Question 6 of 7You fixed the line under a
// @ts-expect-errorcomment. What does TypeScript say now?Choose an answer to see the explanation.
Question 7 of 7TypeScript marks line 8 of this checked file. What does it print when it runs anyway?
Read the code, then predict// @ts-check /** @param {number} quantity */ function addOne(quantity) { return quantity + 1; } console.log(addOne("2"));Choose an answer to see the explanation.
Key takeaways
- TypeScript can check plain
.jsfiles. The types go in JSDoc comments, so the browser runs the file unchanged. // @ts-checkbefore any code checks one file;checkJsin tsconfig.json checks them all;// @ts-nocheckopts one file out.allowJslets.jsfiles into a project, so TypeScript files can import them and use their JSDoc types.@param,@returns,@type, and@typedefhold TypeScript types in curly braces./** @type {T} */ (value), with the brackets, is the JSDoc form ofas, and it's just as unchecked.- Prefer
@ts-expect-errorwith a reason over@ts-ignore: it tells you when the error it hides is gone.
Remember the one-liner.
JSDoc holds the types, // @ts-check or checkJs turns on the errors, and the browser runs the same .js file.
Coming next: Typing the DOM. It covers the types of the page itself: elements, what querySelector gives back, and the null a lookup can return.