JavaScript lets you pass anything anywhere. That freedom is useful until a function receives undefined something where it expected a string, and you find out from a user instead of from your editor. TypeScript fixes that by adding types to JavaScript: you describe what your values should look like, and the compiler checks your code before it ever runs.
TypeScript doesn't replace JavaScript. Every JavaScript file is already valid TypeScript, and what you ship is still plain JavaScript, produced by the TypeScript compiler, tsc. What you gain is earlier error messages, better autocomplete, and code that is easier to change without breaking something you forgot about.
This guide covers why TypeScript helps, setting up a project, the syntax you'll use most, configuring the compiler, and running the code it produces. It ends with a more practical everyday workflow, migrating an existing project, and the problems beginners run into most. You only need Node.js installed and some familiarity with JavaScript.
Why TypeScript Helps
Here is a small JavaScript function with a bug that nothing warns you about:
function totalPrice(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
totalPrice([{ name: "Book", price: "12" }]); // "012", not 12
The price arrived as a string, so JavaScript joined the values as text instead of adding them. The code runs and returns the wrong answer.
The same function in TypeScript:
interface Item {
name: string;
price: number;
}
function totalPrice(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
totalPrice([{ name: "Book", price: "12" }]);
// Error: Type 'string' is not assignable to type 'number'.
The mistake is reported in your editor as you type, before the code runs. That is the core of what TypeScript does, and it scales: the bigger the codebase, the more of these mistakes it catches, and the safer it becomes to rename, move, and refactor things.
Setting Up a TypeScript Project
Create a folder for your project and install TypeScript as a development dependency:
mkdir my-first-ts
cd my-first-ts
npm init -y
npm install --save-dev typescript
Installing it in the project, rather than globally, means everyone working on the project uses the same TypeScript version. You run it through npx, which uses the project's copy:
npx tsc --version
Then generate a configuration file:
npx tsc --init
This creates tsconfig.json, which tells the compiler how to check and build your code. The next sections cover what to put in it.
A Quick Look at the Syntax
Most of TypeScript is JavaScript with type annotations added after a colon:
let username: string = "Ada";
let attempts: number = 3;
let isAdmin: boolean = false;
function greet(name: string): string {
return `Hello, ${name}!`;
}
You don't have to annotate everything. TypeScript infers types from values, so let count = 0 is already known to be a number, and assigning "zero" to it later is an error.
For objects, describe their shape once with an interface and reuse it:
interface User {
id: number;
name: string;
email?: string; // the ? makes it optional
}
function welcome(user: User) {
console.log(`Welcome back, ${user.name}`);
}
Arrays and values that can be one of several types are just as short:
let tags: string[] = ["typescript", "javascript"];
let id: string | number;
id = 42; // fine
id = "a-42"; // also fine
type Status = "draft" | "published" | "archived";
let status: Status = "draft";
// status = "deleted"; // Error: not one of the allowed values
That last pattern, a type made of a few exact values, is one of the most useful things in TypeScript: the compiler and your editor both know every value status can have.
Configuring the Compiler
The generated tsconfig.json contains many options, most of them commented out. To start, three settings matter most: where your source files are, where the compiled JavaScript goes, and how strict the checking is.
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build",
"strict": true
}
}
rootDiris the folder TypeScript reads your.tsfiles from.outDiris the folder it writes the compiled.jsfiles to.strictturns on TypeScript's full set of checks. It's worth having from the start: turning it on in an existing project later means fixing every error it finds at once.
Keep the other options tsc --init set (such as target and module) unless you have a reason to change them. They decide which JavaScript version and module format the output uses, and the defaults suit a modern Node.js project.
Compiling and Running Your Code
Create src/index.ts:
// src/index.ts
function greet(name: string) {
console.log(`Hello, ${name}!`);
}
greet("John");
Compile the project:
npx tsc
TypeScript checks the code and, if there are no errors, writes the result to the outDir folder:
build/index.js
You can run the compiled JavaScript with Node.js:
node build/index.js
For browser applications, the compiled JavaScript is normally loaded by an HTML page or processed further by a build tool such as Vite.
Running tsc after every change gets tedious, so let it watch your files and recompile whenever you save:
npx tsc --watch
This gives you a simple development cycle:
Write TypeScript → Compile with tsc → Run the generated JavaScript
For larger projects, you will usually add tools that make this faster.
A More Practical TypeScript Workflow
Compiling with tsc and running the output works well for learning, but most projects use tools that run TypeScript more directly during development.
For Node.js, tsx (or the older ts-node) runs a .ts file in one step, without a separate build:
npx tsx src/index.ts
Recent versions of Node.js can also run many .ts files directly by stripping the types. One thing to know about all of these: they remove the types without checking them. They are for running code quickly, and you still run npx tsc (or let your editor check as you type) to catch type errors.
Frontend projects commonly use Vite, which handles TypeScript, a development server, hot reloading, and production builds:
npm create vite@latest my-app
Then choose a TypeScript template when prompted.
A typical modern workflow looks like:
Write TypeScript
↓
Development tool / bundler
↓
Browser or Node.js
You don't need to learn Vite or a bundler when you're first learning TypeScript. Understanding tsc first is useful, because it shows what TypeScript actually does: it checks your code and produces JavaScript that can be executed.
Migrating an Existing JavaScript Project
You don't have to convert an entire JavaScript project to TypeScript at once. TypeScript supports gradual migration, which is useful for existing applications.
First, install TypeScript:
npm install --save-dev typescript
Then create a tsconfig.json that lets TypeScript work alongside your existing JavaScript:
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": true,
"outDir": "build"
},
"include": ["src/**/*"]
}
With allowJs enabled, TypeScript includes your existing .js files in the project. checkJs also reports type problems in those files, so you see where the trouble is before converting anything. If that produces too many errors at first, leave checkJs off and turn it on once a few files are converted.
From there, rename files one at a time:
app.js → app.ts
utils.js → utils.ts
server.js → server.ts
Once a file is a .ts file, add type annotations to it and fix what the compiler reports. A gradual migration looks like this:
Existing JavaScript project
↓
Enable TypeScript (allowJs)
↓
Check existing .js files (checkJs)
↓
Convert files one at a time
↓
Add types and fix errors
↓
Continue until no .js files are left
This lets you introduce TypeScript without rewriting an entire application in one step, and the application keeps working the whole way through.
Common TypeScript Problems Beginners Encounter
TypeScript prevents many errors, but its type system can feel frustrating at first. Three problems come up again and again.
Reaching for any. When TypeScript can't work out a value's type, it's tempting to use any to make the error go away:
let data: any = getData();
This works, but it tells TypeScript to stop checking that value, and using any everywhere removes most of TypeScript's benefits. When you genuinely don't know a value's type, use unknown instead. TypeScript then makes you check the value before you use it:
let data: unknown = getData();
if (typeof data === "string") {
console.log(data.toUpperCase()); // safe: TypeScript knows it's a string here
}
Libraries without types. Many packages include their own type definitions, but some older JavaScript packages don't. For those, the community often publishes separate definitions you can install:
npm install --save-dev @types/package-name
A wall of errors from strict mode. Turning on strict in an existing project can produce a lot of errors at once:
{
"compilerOptions": {
"strict": true
}
}
These errors can seem inconvenient, but they usually point to places where your code makes assumptions that could break later, such as a value that might be undefined. Instead of switching strict checking off, work out why TypeScript is reporting each problem and decide whether the code or its types should change.
The goal isn't to make every TypeScript error disappear as quickly as possible. It's to make the type information accurate enough that the compiler can find problems for you before your application runs.
Conclusion
TypeScript gives JavaScript developers stronger type checking, better editor support, and code that is easier to maintain. The basic workflow is to write TypeScript, check and compile it with tscand run the JavaScript it produces.
As projects grow, tools such as tsx and Vite make development faster, while tsc keeps doing the type checking. If you have an existing JavaScript application, you can migrate one file at a time rather than rewriting everything at once.
The most important thing is to understand what TypeScript is doing for you: it adds a type system on top of JavaScript, so that many problems are caught while you're writing the code instead of after it's running.



