TypeScript – Get Started

July 7, 20268 min readUpdated 9/5/2026

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 match

In 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:

ToolVersion
TypeScript6.0.3 (React app) · 5.9.3 (Angular app)
Node22.23.2
React · Vite · Redux Toolkit19.2.8 · 8.2.0 · 2.12.0
Angular · NgRx · RxJS21.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

Shaping data

Behaviour

Real projects

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.