TypeScript is JavaScript with a type checker bolted on and then taken away again. That sentence is worth unpacking, because almost every surprise people hit in their first month comes from missing one half of it.
What it actually is
TypeScript is not a language that runs. Nothing executes TypeScript — not Node, not any browser. What happens is that a compiler reads your annotated code, checks it, and then deletes every annotation and emits ordinary JavaScript.
Here is the whole idea in one file. This is TypeScript:
export const TAX_RATE = 0.085;
export const DELIVERY_FEE = 3.99;
export function formatMoney(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
}And this is what ships to the browser:
export const TAX_RATE = 0.085;
export const DELIVERY_FEE = 3.99;
export function formatMoney(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
}The : number and the : string are gone. They were never for the
runtime; they were for the compiler, and the compiler has finished with them.
Two consequences follow immediately, and they matter more than any syntax you will learn.
Your types cannot check anything at runtime. If you declare that a function
returns a User and the server sends back something else, TypeScript will not notice
and cannot notice. It checked your code; it has no opinion about the network.
TypeScript costs your users nothing. No library is downloaded, no wrapper runs around your functions, the bundle does not grow. Whatever price you pay, you pay it at build time.
What it catches
Roughly three things, in descending order of how often they actually bite.
Typos and shape errors. The largest category by far, and the least glamorous.
You wrote order.totl, or you passed an object missing a field, or a function's
arguments were in the wrong order. These are found the moment you type them rather than when a
customer finds them.
Nulls. With strictNullChecks on, a value that might be missing has
to say so in its type, and you cannot use it until you have handled the missing case. This is a
large fraction of the runtime errors a JavaScript app produces, moved to compile time.
Changes that do not land everywhere. This is the one people underrate. Rename a field on a shared type and the build fails in all nine places that used it, including the two you had forgotten about. Refactoring a large JavaScript codebase is guesswork with grep; in TypeScript it is a task with a definition of done.
What it does not catch
It is worth being precise about this, because overconfidence in the type checker causes its own bugs.
Anything that crosses the boundary into your program is unchecked. An HTTP response, a
localStorage value, a URL parameter, a message from another window — you can
declare what shape you expect, and TypeScript will believe you. It has no way to verify it.
// This compiles. It says nothing about what the server actually sent.
const user = await api.get<User>('/api/auth/me', { auth: true });Nor does it catch logic errors. subtotal - tax typechecks perfectly and is still
wrong. Types constrain shape, not meaning.
A real example of the difference
Abstract arguments about type safety are unconvincing, so here is a concrete one from the application this track uses. A cart line looks like this:
export interface CartItem {
lineId: string;
productId: UUID;
productName: string;
// ...
size: SizeName;
basePrice: number;
// ...
crustPriceDelta: number;
toppings: Topping[];
quantity: number;
}And the function that prices one:
export function unitPrice(item: CartItem): number {
const toppingsTotal = item.toppings.reduce((sum, t) => sum + t.price, 0);
return round2(item.basePrice + item.crustPriceDelta + toppingsTotal);
}Now suppose someone renames crustPriceDelta to crustSurcharge, because
it reads better. In JavaScript, item.crustPriceDelta quietly becomes
undefined, and number + undefined is NaN. The pizza does not
crash the page — it costs $NaN, which renders, and which somebody eventually reports
as "the price is broken sometimes".
In TypeScript the rename fails the build, at every site, before anything runs. That is the entire value proposition in one field.
Types are about shape, not names
One idea to carry into everything that follows, because it explains behaviour that otherwise looks arbitrary: TypeScript's type system is structural. Two types are compatible if their shapes are compatible. What they are called is irrelevant.
interface Product { id: string; name: string; }
interface Snack { id: string; name: string; }
const cheese: Product = { id: 'p1', name: 'Cheese' };
const snack: Snack = cheese; // fine — the shapes matchIn Java or C# that assignment is an error: Product is not Snack,
regardless of what is inside them. TypeScript does not work that way, and it is not a shortcut —
it is the correct model for JavaScript, where objects are bags of properties and nothing carries a
class name at runtime that the checker could rely on.
The practical upshot is that you can describe data you do not own. You never have to make the server's JSON "implement" anything; you write down the shape you expect and TypeScript checks your usage against it.
You do not have to annotate everything
The most common beginner mistake is writing types the compiler already knows.
// Noise. TypeScript already knows both of these.
const taxRate: number = 0.085;
const label: string = formatMoney(total);The literal 0.085 is a number; formatMoney is declared to return a
string. Saying so again adds nothing and gives you a second place to be wrong. What the app
actually writes is this:
export const TAX_RATE = 0.085;
export const DELIVERY_FEE = 3.99;Inference does most of the work. In practice you annotate three things: function parameters, which cannot be inferred because the compiler has no idea who will call you; the shapes of data crossing a boundary; and the occasional return type you want pinned down deliberately. Everything else, let it infer.
What to do about the boundary
Since the checker cannot see the network, the honest position is that
api.get<User>(…) is a claim, not a guarantee. There are three ways teams
handle that, and it is worth knowing where you stand.
Trust the contract. The API is yours, its DTOs are generated from the same schema, and a mismatch is a deployment bug you would find immediately. This is what the pizza app does, and its types file says so out loud:
/**
* Shared domain types.
*
* These mirror the Spring Boot API's DTOs exactly. Keeping one file as the contract means that if
* the shapes drift, TypeScript fails the build instead of the UI failing at runtime.
*/Note what that comment is careful not to claim. Keeping one file means a drift shows up in one place instead of nine — it does not mean the compiler checked the server.
Generate the types. From an OpenAPI document or a GraphQL schema. The types still are not verified at runtime, but they cannot be wrong by hand.
Validate at the edge. A schema library parses the response and either returns a value you genuinely know the shape of, or throws. This is the only one of the three that is actually a guarantee, and it costs you a runtime dependency and a schema written twice.
All three are defensible. What is not defensible is not knowing which one you are relying on.
How you will experience it day to day
Mostly, not as a build step. The same type information drives your editor, so the feedback arrives as you type: completion that knows the fields of the object under your cursor, rename across a project, jump-to-definition that works, and the red underline appearing on the line you just broke rather than in a terminal two minutes later.
That is worth saying because it changes what the annotations are for. Writing
item: CartItem is not paperwork filed for the compiler's benefit. It is what makes
item. offer you twelve real fields instead of nothing.
The versions this track uses
Everything here was run on these, rather than assumed:
| Tool | Version |
|---|---|
| TypeScript | 6.0.3 (React app) · 5.9.3 (Angular app) |
| Node | 22.23.2 |
| React · Vite · Redux Toolkit | 19.2.8 · 8.2.0 · 2.12.0 |
| Angular · NgRx · RxJS | 21.2.0 · 21.1.1 · 7.8 |
The two TypeScript versions are not an oversight. Angular pins its own compiler and had not moved to 6.x, and the two apps are configured with flags that contradict each other — the React one forbids decorators, the Angular one is built on them. Several lessons here are much sharper because of that, and we will come back to it.
The application every example comes from
The snippets in this track are not written for the track. They are lifted from a working pizza ordering application — a storefront, a cart, Stripe checkout, an order history and an admin area — built twice over the same Spring Boot API: once in React and once in Angular.
That matters for a language tutorial specifically. It is easy to demonstrate a type system with examples invented to demonstrate a type system, and the result teaches you syntax you will never reach for. Everything below is code that runs, and where a rule is shown being broken it is labelled as such.
The lessons, in order
Getting started
- Setting Up a Project — two projects, two setups, and why one of them never emits a file
- The Basic Types — and the annotations you should not write
- any, unknown, never and void — the four types that are not values
Shaping data
- Arrays and Tuples
- Object Types — optional, readonly, index signatures, excess property checks
- Interfaces vs Type Aliases — the three differences that are real
- Unions and Intersections
- Enums, and What to Use Instead
- Narrowing and Type Guards
Behaviour
- Functions
- Classes
- Generics
- The Utility Types Worth Knowing
- keyof, Mapped and Conditional Types
- Assertions, as const and satisfies
Real projects
- Modules and Type-Only Imports
- tsconfig, the Flags That Matter
- Decorators
- TypeScript with React
- Interview Questions
Is it worth it?
For a script, no. For anything with more than one contributor or a lifespan beyond a few weeks, the question answers itself the first time you rename a field.
The honest cost is not the annotations — most of those you do not write, because the compiler infers them. It is that you will occasionally spend twenty minutes describing something to the type system that you already understand perfectly. That is the trade: some friction now, in exchange for a class of bug that never reaches production and a codebase that can be changed without fear.
Next
Setting Up a Project — getting a compiler running, and the difference between a setup that emits JavaScript and one that only checks it.