What TypeScript Actually Is
TypeScript is JavaScript with a type system added on top. Every line of valid JavaScript is already valid TypeScript, so you are not learning a new language. You are learning a set of annotations that you write alongside the JavaScript you already know, plus a program called tsc that reads those annotations and complains when your code contradicts them.
That program is a checker, not a runtime. It reads your .ts files, works out what type every value is supposed to be, prints errors for the places where the answer does not add up, and then writes out ordinary JavaScript with all the type information deleted. The browser and Node.js never see a single type annotation. This single fact explains most of what is surprising about TypeScript, and we come back to it twice in this lesson.
The word people use for this is static typing. "Static" simply means "decided by reading the code, before it runs", as opposed to dynamic typing, where a value's type is only discovered while the program is executing. JavaScript is dynamically typed and stays that way; TypeScript adds a static layer on top so that a whole class of mistakes is caught while you are still typing.
- A superset of JavaScript — paste any working
.jsfile into a.tsfile and it still runs - Statically checked — mistakes are reported by the compiler and by your editor, before you run anything
- Erased at build time — the output is plain JavaScript with no runtime cost and no extra library to ship
- Excellent for editors — autocomplete, go-to-definition and safe renaming all come from the type information
- Practically the default for serious JavaScript projects — Angular is written in it, and almost every popular npm package now ships type definitions
The Problem It Solves
JavaScript never objects to a value of the wrong type. It converts, guesses, or produces undefined, and your program keeps running with wrong data inside it. The bug does not appear where the mistake was made; it appears three functions later, often in front of a user.
Take a realistic case. A form gives you the quantity a customer typed. Values read out of an HTML input are always strings, even when they look like numbers. You pass that value into a function that calculates a total, and JavaScript's + operator happily joins two strings instead of adding two numbers. Nothing crashes. The order total is simply wrong, and you find out from a customer.
TypeScript refuses that call at the point where it is written. The message is not clever — it is a plain statement that a string was supplied where a number was expected — but you get it in your editor, with a red underline, seconds after typing it, instead of a week later from a support ticket.
// Plain JavaScript: this runs, and quietly produces nonsense
function total(price, quantity) {
return price * quantity;
}
const qty = document.querySelector("input").value; // always a string, e.g. "3"
total(499, qty); // 1497 — works by luck, because * coerces
total(499, qty + 1); // "31" -> 15469. Wrong, and no error anywhere.
// TypeScript: the mistake is refused where it is made
function totalTs(price: number, quantity: number): number {
return price * quantity;
}
const raw: string = "3";
totalTs(499, raw);
// ERROR: Argument of type 'string' is not assignable to parameter of type 'number'.
totalTs(499, Number(raw)); // OK — you are forced to convert on purpose Types Are Erased — Read This Twice
This is the idea beginners get wrong most often, so it is worth stating bluntly: types do not exist while your program is running. They are deleted during compilation. There is no type checking in the browser, no type checking in Node.js, and no way for TypeScript to inspect a value at runtime and reject it.
The consequence is uncomfortable. When you declare that a function returns a User, you are describing what you believe. If the data actually comes from an API, a database, a JSON file or localStorage, TypeScript has no idea what really arrived. It trusts your annotation completely. If the server renames a field, your code still compiles perfectly and still breaks at runtime.
So the honest description of TypeScript is: it checks that your code is internally consistent with the assumptions you wrote down. It does not check that those assumptions match reality. Anything crossing the boundary into your program — an HTTP response, a form submission, a URL parameter, a message from another window — needs a real runtime check written in ordinary JavaScript, or a validation library that performs one. The lesson on type guards shows how to write those checks properly.
interface User {
id: number;
name: string;
}
async function loadUser(): Promise<User> {
const res = await fetch("/api/user");
return res.json(); // res.json() is typed as any — TypeScript just accepts it
}
// This compiles with zero errors. It is still a promise you cannot keep:
// if the server sends { id: "7", fullName: "Ananya" }, nothing complains here,
// and the crash happens later, wherever user.name is finally used.
const user = await loadUser();
console.log(user.name.toUpperCase()); // TypeError at runtime: undefined - A useful way to hold this in your head: TypeScript is spell-check for your code, not a security guard at the door. It reads what you wrote and finds contradictions. It cannot inspect the parcel that arrives at runtime.
TypeScript Checks Shapes, Not Names
Languages like Java and C# use nominal typing: a value fits a type only if it was explicitly declared as that type, by name. TypeScript uses structural typing instead, often nicknamed duck typing — if it has the right properties, it fits. The name you gave the type is documentation for humans; the compiler only compares the shape.
That is why the object below is accepted even though nobody ever wrote the word Point next to it. It has an x and a y, both numbers, so it satisfies Point. Extra properties are fine too — an object that has more than required still has everything required.
There is one twist that confuses everybody once. When you pass an object literal directly, TypeScript applies an extra rule called the excess property check and rejects unknown properties. The reasoning is that a literal written on the spot has no other purpose, so a stray property is almost always a typo rather than a deliberate extra. Assign the same literal to a variable first and the check does not apply, because the variable might legitimately be used elsewhere.
interface Point {
x: number;
y: number;
}
function draw(p: Point) {
console.log(p.x, p.y);
}
// No 'implements', no inheritance — the shape is enough
const marker = { x: 10, y: 20, label: "start" };
draw(marker); // OK
// But a literal passed straight in is checked more strictly:
draw({ x: 10, y: 20, label: "start" });
// ERROR: Object literal may only specify known properties,
// and 'label' does not exist in type 'Point'. What TypeScript Will Not Do For You
Being clear about the limits early will save you from disappointment and from arguments you cannot win. TypeScript is a narrow tool that does one job extremely well, and people routinely expect four other jobs from it.
Most importantly, it does not make your code correct. A function that returns number and computes the average wrongly will typecheck perfectly. Types constrain the shape of your data, not the meaning of your logic. You still need tests.
- It does not validate data at runtime — an API response is only checked if you write the check yourself
- It does not make code faster; the output is the same JavaScript you would have written
- It does not catch logic bugs, off-by-one errors, or wrong business rules
- It can be switched off accidentally — one
anyin the wrong place disables checking for everything downstream of it - It trusts the type definitions shipped with third-party packages, and those are occasionally wrong or out of date
- It adds a build step, and on a large project the checker takes real time to run
- None of this is an argument against TypeScript. It is an argument for knowing exactly which mistakes it catches — mismatched shapes, forgotten
nullchecks, renamed fields, wrong argument order — and not assuming it catches the rest.
How You Will Actually Use It
In day-to-day work you rarely run the compiler by hand. Your editor runs the same checking service in the background, which is why you see errors and autocomplete as you type. Build tools such as Vite, Next.js and esbuild simply strip the type annotations out and never check anything, because stripping is fast and checking is slow. The check runs separately: once in your editor while you work, and once more in continuous integration with tsc --noEmit before anything is merged.
Because of that split, code with type errors can still build and run locally. That surprises people who assume a broken type means a broken build. It also means a project without a tsc --noEmit step in CI can slowly fill up with errors nobody notices.
The rest of this course follows the order you would meet these ideas in a real project: describing values with annotations, describing objects with interfaces and type aliases, handling values that might be missing, narrowing unions safely, and finally writing reusable typed code with generics.
- Write
.tsfiles (or.tsxwhen they contain JSX for React) - Read errors in your editor as you type — that is the same compiler, running in a background service
- Run
npx tsc --noEmitto check the whole project without producing any output files - Let your bundler or framework strip the types when it builds; runtimes such as Deno and Bun can execute TypeScript files directly
- Add the check to CI so a type error cannot be merged
- Do not aim for zero
anyon day one. A workable path is: turn onstrictfor new files, type the boundaries of your program first (function parameters, API responses, component props), and let inference handle the middle. The next lesson sets up a project that does exactly that.
