Setting up a TypeScript project
Install the TypeScript compiler, write a tsconfig.json, run tsc and Node in a project terminal, and see which tools check types and which only remove them.
- 01Install and run tscAdd TypeScript to a project with npm, and run its compiler with npx and npm scripts.
- 02Write a tsconfig.jsonSet target, strict, rootDir, outDir, and include, and know when tsc reads the file and when it skips it.
- 03Pick where to check and runRun TypeScript in Node, check it in your editor and the Playground, and tell the tools that check types from the ones that only remove them.
From an empty folder to a checked project
So far, TypeScript has run inside this page. To use it for your own work, you set up a project: a folder on your computer with the compiler installed and a file of settings. This lesson builds one for a small shop. Here is the only code it has, a cart that adds up two prices:
type CartItem = { name: string; price: number }; const cart: CartItem[] = [ { name: "Tea", price: 3 }, { name: "Cake", price: 4 },]; let total = 0;for (const item of cart) { total += item.price;}console.log(`Total: ${total}`);Line 1 says what a cart item is: a name that is text and a price that is a number. Lines 3 to 6 make a cart of tea and cake. Lines 8 to 11 add up the prices, and line 12 prints Total: 7. There are no errors. The rest of this lesson is about everything around this file: how to install the compiler, how to tell it what to do, and where to run it.
When the project is set up, the folder looks like this:
shop/
package.json
tsconfig.json
src/
cart.ts
dist/
cart.jspackage.jsonlists the packages the project uses, includingtypescript. (The npm & package.json lesson explains this file.)tsconfig.jsonholds the compiler's settings: how strict to be, which files to check, and where to write the JavaScript.src/cart.tsis the code you write.srcis short for "source".dist/cart.jsis the JavaScript the compiler writes.distis short for "distribution": the files you hand to a browser or to Node.
npx tsc reads the settings in tsconfig.json, checks src/cart.ts, and writes dist/cart.js.A bread maker sits on the kitchen counter. The first time, you choose the loaf size and the crust, and it saves them. After that, you press one button each morning, and it bakes the same way every time. Nobody has to remember the settings.
- In real life: You put a bread maker in your kitchen
- In TypeScript:
npm install --save-dev typescriptadds the compiler to the project - In real life: You set its program once: loaf size, crust, timer
- In TypeScript:
tsconfig.jsonholds the settings - In real life: Each morning you press Start
- In TypeScript: Each build, you run
npx tsc - In real life: It bakes with the saved program
- In TypeScript:
tscreadstsconfig.jsonand follows it
A TypeScript project is a folder with the compiler installed and a tsconfig.json file of settings. Run npx tsc with no file names, and the compiler reads tsconfig.json, checks the files it lists, and writes JavaScript where it says.
Installing the compiler
You need Node.js, the program that runs JavaScript outside a browser. Installing it also installs npm, the tool that downloads packages. Open a terminal, the window where you type commands, and type these four lines:
mkdir shop
cd shop
npm init -y
npm install --save-dev typescript@5.9- Line 1 makes a folder called
shop, and line 2 moves into it. - Line 3 makes a
package.json. The-yanswers "yes" to every question npm would ask. - Line 4 downloads the
typescriptpackage into a folder callednode_modules. The--save-devoption lists it as a dev dependency: something you need while you build, but the finished page doesn't. The@5.9asks for version 5.9, the one this site checks its examples with, so what you see matches this lesson. Leave it out to get the newest version.
Here is the part of package.json the install added:
{
"name": "shop",
"version": "1.0.0",
"devDependencies": {
"typescript": "5.9"
}
}Lines 4 to 6 are the new part. "5.9" means "any 5.9 version". npm also adds a few fields that don't matter here, like a description and a license, which are left out above. Now check that the compiler is there:
npx tsc --versionVersion 5.9.3npx runs a command from the packages in this project. So npx tsc means "the tsc that belongs to this folder". It prints the version npm installed, here 5.9.3. A teammate who runs npm install on the same package.json gets a 5.9 version too, so everyone checks the code the same way.
You could install one copy for the whole computer with npm install -g typescript, and then type tsc on its own. Projects avoid it: that copy isn't written in package.json, and two projects might need different versions.
Most projects also give their commands short names, called scripts, in package.json. Inside a script you write tsc without npx, because npm already looks in the project's packages:
{ "name": "shop", "version": "1.0.0", "scripts": { "build": "tsc", "check": "tsc --noEmit" }, "devDependencies": { "typescript": "5.9" }}Line 5 names tsc as the build script. Line 6 names tsc --noEmit as the check script. --noEmit means "check, but don't write any files". You run a script with npm run and its name:
npm run check
npm run buildThe table below compares the ways to start the compiler. Read each row as: what you type, what runs, and when to use it.
| When you type | What runs | When to use it |
|---|---|---|
npx tsc | The copy of TypeScript inside this project's node_modules folder. | Any time you're in the project's folder. |
npm run build | The build script in package.json, which runs the project's tsc. | When the project has scripts, so everyone types the same command. |
tsc on its own | A copy installed for the whole computer with npm install -g, if there is one. | Rarely. It may be a different version from the project's. |
tsconfig.json: the project's settings
tsc has dozens of options. Instead of typing them every time, you write them once in a file called tsconfig.json at the top of the project. TypeScript can write a starting file for you:
npx tsc --init{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
// "rootDir": "./src",
// "outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": [],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
}
A line starting with // is a comment: a note, or a setting that is switched off. Plain JSON doesn't allow comments, but tsconfig.json does. Every setting sits inside "compilerOptions", which starts on line 3. Line 36, "strict": true, is the most important one, and the next section is about it. The rest are choices for later lessons, and you can delete what you don't understand yet.
The shop project uses a much smaller file:
{
"compilerOptions": {
"target": "es2022",
"strict": true,
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}- Line 3,
target, picks the edition of JavaScript tsc writes.es2022is the 2022 edition, which every current browser and Node runs, so the output keepsconstandlet. - Line 4 turns on
strictchecks. - Line 5,
rootDir, says the code lives insrc. Line 6,outDir, says to write JavaScript intodist. Sosrc/cart.tsbecomesdist/cart.js. - Line 8,
include, lists the files that belong to the project: everything insrc. It sits outsidecompilerOptions, because it says which files, not how to compile them.
The table below sums up the five settings. Read each row as: the setting, what it says, and its value in the shop.
| Setting | What it says | In the shop project |
|---|---|---|
target | Which edition of JavaScript tsc writes. | es2022: modern JavaScript that keeps const and let. |
strict | Whether to turn on the full set of stricter checks. | true: TypeScript reports more mistakes. |
rootDir | The folder your .ts files live in. | src. |
outDir | The folder tsc writes JavaScript into. | dist, so src/cart.ts becomes dist/cart.js. |
include | Which files belong to the project. | Every file in src. |
tsc reads tsconfig.json only when you give it no file names. Try it: run the commands below in any order and watch the terminal and the list of files.
{ "compilerOptions": { "target": "es2022", "strict": true, "rootDir": "src", "outDir": "dist" }, "include": ["src"]}0$yoursyoursyoursPress a command to run it in the shop folder. The terminal and the list of files update after each one.
Press npx tsc and dist/cart.js appears, with const and a first line of "use strict", which turns on JavaScript's strict mode. Then node dist/cart.js prints Total: 7; before the build, Node can't find the file. npx tsc --noEmit checks and writes nothing. And npx tsc src/cart.ts writes src/cart.js with var: with a file name, TypeScript 5.9 ignores tsconfig.json and uses its own defaults.
npx tsc follows tsconfig.json. Name a file, and TypeScript 5.9 skips tsconfig.json and writes old-style JavaScript next to your code.That last one surprises people, because nothing warns you. TypeScript 6.0 and later refuse instead, with error TS5112: "tsconfig.json is present but will not be loaded if files are specified on commandline". In any version, the fix is the same: run npx tsc with no file names.
Turning on strict checks
"strict": true is one switch for a whole group of stricter checks. Two of them matter right away. A parameter with no type, like price below, quietly gets the type any, which means "don't check this"; strict reports that as an error. And a value that might be undefined must be checked before you use it. Flip the switch and watch the errors:
{ "compilerOptions": { "strict": true }}function lineTotal(price, quantity: number) {// ^? return price * quantity;} const cart = [ { name: "Tea", price: 3 }, { name: "Cake", price: 4 },];const cake = cart.find((item) => item.name === "Cake");console.log(lineTotal(cake.price, 2));82 errors: lines 1 and 11anyTwo errors. Line 1: Parameter 'price' implicitly has an 'any' type. Nothing says what price is, so TypeScript gave it the type any, which means "don't check this". Line 11: 'cake' is possibly 'undefined', because find gives back undefined when nothing matches. The code still prints 8.
With strict on, TypeScript marks two lines. Line 1 gets Parameter 'price' implicitly has an 'any' type: "implicitly" means TypeScript chose any because you didn't write a type. Line 11 gets 'cake' is possibly 'undefined', because find gives back undefined when no item matches (Searching arrays covers find). With strict off, both errors disappear, but nothing changed in the code. The query on line 2 shows price is still any either way. Strict didn't make the code safer or riskier; it decided whether TypeScript tells you.
Turn strict on at the start of every new project. Turning it on later means fixing every error it finds at once. One more reason to write it: in TypeScript 5.9, strict is off unless tsconfig.json turns it on, and TypeScript 6.0 and later turn it on by default. Writing "strict": true makes the project behave the same in every version. A later lesson, Strict mode options, goes through every check strict turns on.
Running TypeScript in Node
There are two ways to run cart.ts in Node. The first is the one you've already used: build, then run the JavaScript.
npx tsc
node dist/cart.jsLine 1 checks the project and writes dist/cart.js. Line 2 runs that file and prints Total: 7.
The second is newer. Node 22.18 and later, and Node 23.6 and later, run a .ts file directly. (Node 22.6 to 22.17 need the option --experimental-strip-types.)
node src/cart.tsNode does this with type stripping: it replaces every type with spaces and runs what's left. That's quick, but it skips two things. Node never checks the types, and it never reads tsconfig.json. Here is what that means for a cart with a mistake on line 5:
type CartItem = { name: string; price: number }; const cart: CartItem[] = [ { name: "Tea", price: 3 }, { name: "Cake", price: "4" },]; let total = 0;for (const item of cart) { total += item.price;}console.log(`Total: ${total}`);node src/cart.tsTotal: 341 error: line 5yesNode removes the types and runs what's left. It never checks them, so the text "4" gets through, and 3 + "4" joins into 34. No warning at all.
Run with Node prints Total: 34, with no warning. Check with tsc finds the bug. The message is in two parts: the first says what's wrong on line 5, and the indented part shows line 1, where price: number was written. Check, then run joins both commands with &&, so the code only runs when the check passes. The last button fixes line 5, and both commands succeed.
cart.ts. Node removes the types and runs the code, so the text price makes Total: 34. tsc checks the types and reports the bug.Because Node only removes types, it can't run TypeScript features that turn into new JavaScript code. The best-known one is an enum: a named list of values, like sizes, that the Enums lesson covers later. TypeScript has an option that reports these features before Node fails on them:
enum Size { Small, Large,}console.log(Size.Large);Lines 1 to 4 make an enum called Size, and line 5 prints Size.Large. This example turns on erasableSyntaxOnly, and TypeScript marks line 1 on purpose: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. "Erasable" means "can be removed without leaving anything behind". An enum can't: tsc turns it into JavaScript code, and that code prints 1. Node stops at line 1 instead:
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only modeIf you run .ts files with Node, add "erasableSyntaxOnly": true to tsconfig.json, and your editor will point these out as you type. If you need the full language in Node, or an older Node, a package called tsx runs TypeScript files:
npm install --save-dev tsx
npx tsx src/cart.tsLine 1 installs tsx as a dev dependency, and line 2 runs src/cart.ts with it. Like Node, it removes the types without checking them, so keep npx tsc --noEmit for checking.
Editors and the Playground
You don't need to run tsc to see errors while you write. Editors like VS Code have TypeScript built in. They ask it about every file you open, underline errors as you type, list them in the Problems panel, and show a type when you point at a name:
const cart = [ { name: "Tea", price: 3, quantity: 2 }, { name: "Cake", price: 4, quantity: 1 },];const itemCount = cart.length;// ^?console.log(itemCount);Lines 1 to 4 make a cart with two items. Line 5 counts them. The query on line 6 shows what your editor shows when you point at itemCount: const itemCount: number. Line 7 prints 2. Point at cart on this page to see the type of the whole list.
Two details decide whether the editor and tsc agree:
- Both read the same
tsconfig.json. Turn onstrict, and the editor shows the new errors too. - They can use different TypeScript versions. VS Code comes with its own copy. To use the project's, open a
.tsfile and run the command TypeScript: Select TypeScript Version, then choose Use Workspace Version.
One difference matters: the editor checks the files you have open. npx tsc --noEmit checks every file in the project, including ones nobody has opened today. That's why projects run it before every build.
tsc read the same tsconfig.json. The editor checks the files you have open, as you type. In a terminal, npx tsc --noEmit checks every file, each time you run it.When you only want to try an idea, you don't need a project at all. The TypeScript Playground on the TypeScript website runs the compiler in your browser. You type code on the left, and it lists the errors, shows the JavaScript it writes, and runs it. Its TS Config menu changes the settings, its version menu picks the TypeScript version, and Share gives you a link to send someone.
The checked examples on this site work the same way, and each one has its own settings. This one turns strict off, so it has no errors:
function lineTotal(price, quantity) { return price * quantity;}console.log(lineTotal(3, 2));Line 1 declares lineTotal with two parameters and no types. Line 4 calls it and prints 6. Press Pop out: the code opens in the site's editor with a tsconfig.json next to it, holding the example's settings:
{
"compilerOptions": {
"strict": false
}
}Change false to true on line 3 and go back to the code. Line 1 now has two errors, one for price and one for quantity, the same message you saw in the strict playground.
A checkout page project with Vite
Real frontend projects rarely start from an empty folder. A tool like Vite builds the starting files for you (the Bundlers & build tools lesson covers Vite). Its vanilla-ts template is a page written in TypeScript with no framework:
npm create vite@latest checkout -- --template vanilla-ts
cd checkout
npm install
npm run devLine 1 makes a folder called checkout from the template. The -- passes --template vanilla-ts on to Vite's setup tool. Line 3 installs the packages, TypeScript among them. Line 4 starts a local server and prints an address like http://localhost:5173 to open in your browser. The template comes with its own tsconfig.json. Here are the lines that matter now:
{ "compilerOptions": { "target": "es2023", "lib": ["ES2023", "DOM"], "moduleResolution": "bundler", "noEmit": true }, "include": ["src"]}- Line 4,
lib, lists the built-in things TypeScript knows about."DOM"adds the browser's:document, buttons, inputs, and so on. - Line 5,
moduleResolution, tells TypeScript that a bundler like Vite will find the files yourimportlines name. - Line 6,
noEmit, means tsc never writes JavaScript in this project. Vite removes the types itself, file by file, so the page loads fast. tsc is there only to check.
Without "DOM" in lib, a checkout page's code can't even find the page:
const quantityInput = document.createElement("input");quantityInput.value = "2"; const quantity = Number(quantityInput.value);console.log(`Cake x${quantity}: $${quantity * 4}`);Line 1 makes an <input> box, and line 2 puts "2" in it. Line 4 turns the text into a number. Line 5 prints Cake x2: $8. This example sets lib to ["es2023"] alone, and TypeScript marks lines 1 and 5 on purpose: Cannot find name 'document', and then the same for console. Both come from the browser, and "DOM" is how TypeScript learns about them. The message even says what to do: Try changing the 'lib' compiler option to include 'dom'.
Because Vite doesn't check types, the template's scripts run both tools:
{ "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }}Line 3, npm run dev, starts Vite alone, for quick changes while you work; your editor shows the errors. Line 4, npm run build, runs tsc first. With noEmit in tsconfig.json, that's a check. If it finds an error, && stops the build, so a type error never reaches your users. Line 5 serves the built page so you can look at it. The table below lists which tools check types and which only remove them.
| The tool | Does it check types? | Does it remove types and run the code? |
|---|---|---|
npx tsc | Yes, every file in the project. | It writes JavaScript. Node or a browser runs it. |
npx tsc --noEmit | Yes, every file in the project. | No. It writes nothing and runs nothing. |
node src/cart.ts | No. | Yes. Node removes the types and runs the code. |
| Your editor | Yes, the files you have open, as you type. | No. |
| The TypeScript Playground | Yes, the code on the page. | Yes. It shows the JavaScript and can run it. |
| Vite | No. | Yes. It removes the types and serves or bundles the code. |
npx tsc --noEmitnpx tscnode src/cart.tsnpx tsx src/cart.ts- Red underlines in VS Code
npm run devin a Vite project- Errors in the TypeScript Playground
vite buildon its own
Sort each tool by what it does with your types.
Common mix-ups
- "I should install TypeScript once for the whole computer." No. Install it in each project with
npm install --save-dev typescript, sopackage.jsonrecords the version and everyone uses the same one. - "
npx tsc src/cart.tsuses my tsconfig.json." No. With a file name, TypeScript 5.9 skipstsconfig.json, and 6.0 and later stop with an error. Runnpx tscwith no file names. - "
node cart.tschecks my types." No. Node removes the types and runs the code. Check withnpx tsc --noEmit, or both at once withnpx tsc --noEmit && node src/cart.ts. - "
--noEmitmeans tsc does nothing." No. It still checks every file and reports every error. It only skips writing JavaScript. - "Strict is always on." No. In TypeScript 5.9 it's off unless
tsconfig.jsonturns it on. Write"strict": trueyourself so every version behaves the same. - "No red underlines in my editor means the project has no errors." No. The editor checks the files you have open.
npx tsc --noEmitchecks them all.
Practice exercises
Some exercises are about files and commands, and some are TypeScript code. For the code ones, press Edit & run on the starter: the errors update as you type. Try a guess first, then use the hints.
This is the shop's package.json. Which command checks the project without writing any files?
{
"name": "shop",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"check": "tsc --noEmit"
},
"devDependencies": {
"typescript": "5.9"
}
}npm run checkThe check script runs tsc --noEmit, which checks every file and writes nothing. You run it with npm run check. npm run build would also write dist/cart.js.
A teammate changed the shop's tsconfig.json. Where does npx tsc now write src/cart.ts?
{
"compilerOptions": {
"target": "es2022",
"strict": true,
"rootDir": "src",
"outDir": "public/js"
},
"include": ["src"]
}public/js/cart.jsrootDir is src and outDir is public/js, so src/cart.ts becomes public/js/cart.js. tsc makes the folders if they don't exist.
This project has "strict": true, and TypeScript marks line 1. Fix it without turning strict off. What does the fixed code print?
function discounted(price, percent: number) { return price - (price * percent) / 100;}console.log(discounted(20, 10));function discounted(price: number, percent: number) { return price - (price * percent) / 100;}console.log(discounted(20, 10));price: number tells TypeScript what price holds, so it no longer falls back to any and strict has nothing to report. 20 - (20 * 10) / 100 is 20 - 2, which prints 18.
You run this file with node order.ts. TypeScript marks line 1, but Node doesn't care. What does line 3 print? Then fix line 1.
const itemCount: number = "2";console.log(itemCount * 3);console.log(itemCount + 1);const itemCount: number = 2;console.log(itemCount * 3);console.log(itemCount + 1);Node doesn't check types, so itemCount is the text "2". Line 2 prints 6, because * turns the text into a number. Line 3 prints 21, because + joins text. The fix stores the number 2, so the error is gone and line 3 prints 3.
Fill in the start script so that npm start checks the project and then runs src/cart.ts with Node, but never runs code that has a type error.
{
"scripts": {
"start": ""
}
}{
"scripts": {
"start": "tsc --noEmit && node src/cart.ts"
}
}tsc --noEmit checks every file and ends with exit code 2 if it finds an error. && runs node src/cart.ts only after a clean check. In a script, tsc needs no npx.
A teammate ran this command. It wrote src/cart.js full of var, made no dist folder, and reported no strict errors. Which command should they run instead?
npx tsc src/cart.tsnpx tscWith a file name, TypeScript 5.9 ignores tsconfig.json: no outDir, no strict, and the old default target. With no file names, tsc finds tsconfig.json, checks everything in src, and writes dist/cart.js.
Check your understanding
For each question, ask which tool is doing the work, and whether it reads tsconfig.json, checks types, or only removes them. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7Which command adds TypeScript to one project, only for building it?
Choose an answer to see the explanation.
Question 2 of 7You run
npx tscwith no file names. What does it do?Choose an answer to see the explanation.
Question 3 of 7With this tsconfig.json, where does
npx tscwrite src/cart.ts?Read the code, then predictJSON{ "compilerOptions": { "rootDir": "src", "outDir": "build" }, "include": ["src"] }Choose an answer to see the explanation.
Question 4 of 7With "strict": true in tsconfig.json, which line of this file does TypeScript flag?
Read the code, then predictfunction half(price) { return price / 2; } console.log(half(8));Choose an answer to see the explanation.
Question 5 of 7This file has a type error. What does
node src/cart.tsprint for it?Read the code, then predictconst items: number = "2"; console.log(items + 1);Choose an answer to see the explanation.
Question 6 of 7Your editor shows no errors, but
npx tsc --noEmitreports one in a file you haven't opened. Why?Choose an answer to see the explanation.
Question 7 of 7Why does a Vite project's tsconfig.json say "noEmit": true?
Choose an answer to see the explanation.
Key takeaways
- Install TypeScript in each project with
npm install --save-dev typescript, and run it withnpx tscor an npm script. tsconfig.jsonholds the settings.npx tscwith no file names reads it; with a file name, it doesn't.target,strict,rootDir,outDir, andincludeare enough to start a project.- Turn on
strictfrom the start. It reports values with no type and values that might beundefined. node file.tsremoves types and runs the code without checking. Pair it withnpx tsc --noEmit.- Your editor checks open files as you type;
tsc --noEmitchecks every file. The Playground is for trying ideas with no setup.
Remember the one-liner.
Install TypeScript in the project, write your settings in tsconfig.json once, and let tsc check what Node and Vite only run.
Coming next: Your first typed program. You'll turn a plain JavaScript cart into TypeScript, read the compiler's errors, and fix them.