Setup & first types.
Three commands to typed JavaScript.
TypeScript is JavaScript with types. It's not a new language — it's a compiler that checks your types and outputs plain JavaScript. This lesson walks through the setup, explains what tsc actually does, shows you type annotations vs inference, and proves that types cost zero at runtime. No framework, no build tool. Just the compiler and a terminal.
Install TypeScript with npm install -g typescript, verify with tsc --version, write hello.ts, compile with tsc hello.ts, run with node hello.js. That's the entire loop. TypeScript compiles to plain JavaScript by stripping types — the output is what runs. Annotations (: string, : number) are optional; inference handles most cases. Modern TypeScript is 90% JavaScript you already know, 10% type syntax on top.
Before this lesson: you must know JavaScript (functions, arrays, objects, arrow functions, destructuring). If not, complete the JavaScript track first.
⚙️ Setup — three commands
Install the compiler. Compile a file. Run the output. That's the loop.
Before you start, make sure Node.js is installed. Open a terminal and run node --version. If you see a version number (18+), you're set. If not, install Node from nodejs.org — the LTS version is fine.
# 1. Install the TypeScript compiler globally npm install -g typescript # 2. Verify it worked tsc --version Version 5.7.2 # 3. Create a file and compile it echo 'const msg: string = "hi"; console.log(msg);' > hello.ts tsc hello.ts # 4. Run the output (plain JavaScript) node hello.js hi
Notice what happened: hello.ts went in, hello.js came out. Open hello.js — the type annotation is gone. That's the entire story of TypeScript: it checks types, then erases them.
Why tsc?
tsc is the official TypeScript compiler. It checks types and outputs JavaScript. It's what every other tool (Vite, Next.js, webpack) uses under the hood — they just wrap it with a dev server.
What you get
Type checking in your editor, autocomplete that knows every property, refactoring that's safe, and compile-time errors before runtime. The compiler is the whole value proposition.
The two files
hello.ts is what you write. hello.js is what the browser or Node runs. The .ts file never ships. Only the .js does.
Node.js version
TypeScript 5+ runs on Node 14+, but modern tooling (Vite, Next.js) expects Node 18+ or 20+. Check with node --version. Older Node causes cryptic errors.
Prefer a project-local install for real work. The global install is fine for learning. But in a real project, run npm install --save-dev typescript and use npx tsc. This pins the TypeScript version per project, so your teammate's build doesn't break when a new version drops. Global installs drift; local installs are reproducible.
📝 Type annotations — telling the compiler what things are
A colon, a type name. That's the syntax.
A type annotation attaches a type to a variable, parameter, or return value. The syntax is : type after the name. TypeScript then enforces that type — assigning the wrong type is a compile error, not a runtime crash.
// ─── The primitives ─────────────────────────── let name: string = 'Alice'; let age: number = 28; let isActive: boolean = true; // ─── Arrays ─────────────────────────────────── let tags: string[] = ['ts', 'js']; let scores: Array<number> = [90, 85, 77]; // string[] and Arrayare the same thing // ─── Objects (inline shape) ─────────────────── let user: { name: string; age: number } = { name: 'Alice', age: 28 }; // ─── Function parameters and return ─────────── function greet(name: string): string { return Hello, ${name}!`; } // ↑ param ↑ return type // ─── Optional and default params ────────────── function greet2(name: string, greeting?: string): string { return ``${greeting ?? 'Hello'}, ${name}!; } // ↑ optional — string | undefined // ─── The compiler catches mistakes ──────────── let count: number = 5; count = 'five'; // ❌ Type 'string' is not assignable to type 'number' function add(a: number, b: number): number { return a + b; } add('1', '2'); // ❌ Argument of type 'string' is not assignable to parameter of type 'number'
Colon syntax
name: string — the colon is the annotation. Read it as "name is a string." Applies to variables, parameters, return types, class fields, and object properties.
Primitives
string, number, boolean, null, undefined, symbol, bigint. Lowercase — these are the types, not the constructors.
Array syntax
string[] and Array are identical. Use whichever reads better. Nested arrays: string[][]. Readonly: readonly string[].
Optional ?
greeting?: string means string | undefined. The parameter can be omitted. Inside the function, TypeScript forces you to handle the undefined case.
Don't annotate everything. The most common beginner mistake is writing const x: number = 5 — the annotation is redundant. TypeScript infers it. Annotate function parameters (required — no inference available), return types on exported functions, and exported APIs. For local variables, let inference do its job.
🧠 Type inference — the compiler does it for you
You write less than you think. Inference is the default.
TypeScript doesn't require you to annotate every variable. It infers types from the initial value. let x = 5 → number. let name = 'Alice' → string. let items = [1, 2, 3] → number[]. The compiler reads your code and figures it out.
// ─── Inference: no annotation needed ────────── let age = 28; // inferred: number let name = 'Alice'; // inferred: string let isActive = true; // inferred: boolean let tags = ['ts', 'js']; // inferred: string[] let user = { name: 'A', age: 28 }; // inferred: { name: string; age: number } // ─── Return type inference ──────────────────── function add(a: number, b: number) { return a + b; // inferred return: number } // You don't have to write ": number" — TS figures it out // ─── When inference is NOT enough ───────────── // Function parameters: always annotate, no initial value function greet(name) { // ❌ Parameter 'name' implicitly has an 'any' type return Hello,${name}`; } // Empty arrays: annotate to give them a type let items = []; // inferred: any[] — dangerous let items2: string[] = []; // ✅ typed // ─── The rule ───────────────────────────────── // Annotate: parameters, exported return types, empty collections // Infer: local variables, obvious returns, most things
Inference is the default
TypeScript always tries to infer first. You only annotate when inference can't figure it out — or when you want to lock a type down for documentation purposes.
Parameters need annotations
A function parameter has no initial value, so there's nothing to infer from. function add(a, b) → both are any. Always annotate parameters.
Empty arrays are any[]
let items = [] infers any[] — because TypeScript has nothing to go on. Annotate: let items: string[] = []. This is a common source of bugs.
Return types: optional
TypeScript infers return types. For local functions, skip them. For exported/library functions, write them — they catch accidental API changes and serve as documentation.
Hover over any variable in your editor. VS Code shows the inferred type in a tooltip. This is the fastest way to build intuition — write code, hover, see what TypeScript thinks. After a week of hovering, you'll feel what types are inferred without checking. The editor is your teacher.
🎬 How types are erased — zero runtime cost
What you write vs what actually runs. This is the whole mental model.
This is the single most important thing to internalize about TypeScript: types do not exist at runtime. The compiler strips them. What ships to the browser is plain JavaScript — exactly as if you'd written it in JavaScript from the start.
interface User { name: string; age: number; } function greet(user: User): string { return Hello, ${user.name}!`; } const alice: User = { name: 'Alice', age: 28 }; console.log(greet(alice));
// interface: GONE — it only existed for type checking // : User on the param: GONE // : string on the return: GONE // : User on the const: GONE function greet(user) { return Hello,${user.name}!`; } const alice = { name: 'Alice', age: 28 }; console.log(greet(alice));
That's it. The interface disappears. The annotations disappear. What's left is plain JavaScript that runs anywhere. No overhead, no runtime type checking, no penalty.
Zero runtime cost
Types are checked at compile time, then erased. The shipped JavaScript has no type information — same size, same speed as if you'd written JS. Types are a development tool, not a feature.
Types catch bugs early
greet({ name: 'Alice' }) missing age is a compile error. In JavaScript it'd be a runtime crash — or worse, silent undefined behavior. TypeScript finds it before you hit save.
Interfaces don't exist at runtime
You can't instanceof User — there's no User at runtime, it was erased. Use class if you need runtime type. Interfaces are compile-time only.
The output is readable
tsc doesn't obfuscate. It strips types and emits clean JavaScript. You can always read the output to see exactly what ships. No black box.
The mental model that makes TypeScript click. You write TypeScript. The compiler checks it, then throws the types away. What runs is JavaScript. So the question is never "does TypeScript add overhead?" — it can't, there's nothing left. The only question is "does TypeScript catch bugs that JavaScript would miss?" and the answer is always yes. That's the entire trade: a compile step for compiler-checked code.
🏆 Challenge
Three tasks. Each exercises a different TypeScript concept.
Task 1 — Type a function
Write a function calculateTotal that takes:
1. An array of items, each with name: string, price: number, quantity: number
2. A tax rate as a number
3. Returns the total as a number (sum of price × quantity, plus tax)
Start with the inline object type. Then extract it into an interface called LineItem. Notice how the function signature gets cleaner without losing any safety. Call the function with a real array and log the result.
Task 2 — Break it on purpose
Take the function from Task 1. Try these one at a time and read the compiler errors:
1. Pass a string instead of a number for the tax rate
2. Pass an array where one item is missing quantity
3. Pass an array where price is a string
4. Return a string instead of a number
5. Call the function with no arguments
For each, note the exact error. That message format — "Type X is not assignable to type Y" — is the single most common TypeScript error. Learning to read it is half of learning TypeScript.
Task 3 — Inference vs annotation
Write the following three blocks. Then hover over each variable in VS Code and note the inferred type:
1. let x = 5; let y = x + 3; let z = y * 2; — all inferred
2. const user = { name: 'Alice', age: 28, active: true }; — inferred object
3. const items = []; items.push('hello'); — this is a bug. Why? Fix it with an annotation.
The lesson: TypeScript infers more than you think, and the places it can't infer (empty arrays, parameters) are exactly where annotations matter. After this exercise you'll know instinctively when to annotate.
⚠️ Common mistakes
The setup and annotation bugs that catch every beginner.
any to make errors go awaylet x: any = ... disables type checking. You've turned TypeScript off for that value. Fix: use unknown and narrow it, or figure out the real type. any is a last resort.
const x: number = 5 is noise. The compiler infers number. Fix: annotate parameters, exported APIs, empty arrays. Let everything else be inferred.
function add(a, b) — both are any. That's the opposite mistake. Fix: function add(a: number, b: number). Parameters have no initial value, so inference can't help.
let items = [] infers any[]. Push anything — no error. Fix: let items: string[] = [] or let items: User[] = [].
if (typeof x === 'string') works because it's real JS. if (x is string) is a type guard — it's erased. Fix: never write runtime logic that depends on type information existing.
interface exists at runtimeYou can't instanceof User if User is an interface — it doesn't exist in the output. Fix: use a class if you need a runtime value, or a discriminated union tag.
strictDefault tsconfig is permissive — null and undefined sneak through. Fix: add "strict": true in tsconfig from day one. It catches the bugs that matter.
tsc file.ts is fine for learning. In a project, use a build tool (Vite, Next.js, ts-node, tsx). Fix: npx tsx src/index.ts for quick runs, or a proper build pipeline for apps.
❓ Frequently asked questions
How do I install TypeScript?
Install Node.js first, then run npm install -g typescript to install the compiler globally. Check it worked with tsc --version. Then write a file called hello.ts and compile it with tsc hello.ts — that produces hello.js, which you run with node hello.js. Alternatively, install it locally per project with npm install --save-dev typescript.
What does the TypeScript compiler do?
The TypeScript compiler (tsc) does two things: it checks your types and reports errors, and it strips the types to output plain JavaScript. The .ts file goes in, the .js file comes out. The types are erased — they don't exist in the output. Type errors are caught at compile time, before your code ever runs.
What is type inference?
Type inference is TypeScript's ability to figure out a type without you writing it. let x = 5 is inferred as number. let name = 'Alice' is inferred as string. You only need to annotate when TypeScript can't figure it out, like function parameters.
What is a type annotation?
A type annotation is an explicit label after a colon that tells TypeScript what type something is. let age: number = 28; — the : number is the annotation. Annotations are useful for function parameters, function return types, and exported APIs.
Why do types cost zero at runtime?
Because TypeScript erases them during compilation. The output JavaScript contains no type information — no annotations, no interfaces, no generics. Types exist only during development and compilation. Your shipped code is plain JavaScript, so it's the same size and speed as if you'd written JavaScript directly.
Should I use strict mode in TypeScript?
Yes, always. strict: true in tsconfig.json turns on strictNullChecks, noImplicitAny, and several other safety checks. It catches the most common bugs — null and undefined errors, implicit any. Turning it on at the start of a project is easy; turning it on in an existing codebase is painful because it surfaces hundreds of errors.
What is the difference between any and unknown?
any turns off type checking entirely — you can do anything with it, and TypeScript won't complain. unknown is the safe alternative: you can assign anything to it, but you can't use it until you narrow it with a check like typeof or instanceof. Use unknown for values you don't know the type of yet, then narrow. Use any only for interop with untyped libraries.
How do I run TypeScript in the browser?
Browsers can't run TypeScript directly — they only understand JavaScript. You must compile it first. For learning, use the TypeScript Playground at typescriptlang.org/play. For real projects, use a build tool: Vite with the TS template compiles on save and reloads the browser.