Skip to content

TypeScript

TypeScript is JavaScript with a static type system bolted on top. You write annotations that describe the shape of your values, a compiler checks that your program is consistent with those annotations, and then it deletes them and hands plain JavaScript to the runtime.

That last part is the whole story in one sentence: TypeScript is a compile-time tool. It never runs. Nothing it does can protect you at runtime.

JavaScript already has types — string, number, object, and so on — but it only checks them while the program is running, and usually by coercing rather than complaining. TypeScript adds a second, separate type system that exists only in your editor and your build step:

  • Type annotationsconst n: number = 1.
  • Type inference — most of the time you write no annotation and TypeScript figures it out.
  • A structural type checker — it verifies that the values you pass around fit where you put them.
  • Editor intelligence — autocomplete, go-to-definition, safe renames, inline errors. This is the benefit most people actually feel day to day.
  • Syntax JavaScript doesn’t haveinterface, type, enum, generics, as, satisfies, access modifiers on class members.

It does not add new runtime features. There is no TypeScript standard library, no TypeScript Array method that JavaScript lacks. If it exists at runtime, it came from JavaScript.

Type erasure: types have no runtime effect

Section titled “Type erasure: types have no runtime effect”

Compiling TypeScript is mostly an act of deletion. Everything type-related is stripped out.

user.ts
interface User {
id: number;
name: string;
}
function greet(user: User): string {
return `Hello, ${user.name}`;
}
const me: User = { id: 1, name: 'Ada' };
console.log(greet(me));

Compiles to:

user.js
function greet(user) {
return `Hello, ${user.name}`;
}
const me = { id: 1, name: 'Ada' };
console.log(greet(me));

The interface vanished entirely. So did every : Type. The emitted JavaScript is the same code you would have written by hand.

Two consequences follow, and they trip up nearly everyone at some point:

1. You cannot test a type at runtime. There is no if (x is User). The type doesn’t exist anymore. You can only test things JavaScript can test — typeof, instanceof, property presence.

// Does not compile — `User` is not a value.
if (user instanceof User) {} // error: 'User' only refers to a type, but is being used as a value
// This is how you actually check it.
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null && 'id' in x && 'name' in x;
}

2. Types do not validate data. Annotating a value as User is a claim, not a check. If the value comes from the network, a file, or JSON.parse, the annotation is only as true as your assumption.

A handful of TypeScript constructs are not purely type-level and do emit JavaScript:

Construct What it emits
enum A real object at runtime
namespace (with values) An IIFE assigning to an object
Parameter properties (constructor(private x: number)) An assignment in the constructor body
Decorators Runtime function calls

Everything else — type, interface, generic parameters, as, satisfies, declare — is erased.

Every JavaScript file is a valid TypeScript file

Section titled “Every JavaScript file is a valid TypeScript file”

Rename app.js to app.ts and it still compiles. This is deliberate: TypeScript is a superset of JavaScript, so adoption is incremental. You may see new errors — that’s the checker finding real ambiguity — but the syntax is always accepted.

This also means you already know most of TypeScript. Loops, closures, async/await, classes, destructuring, modules: all of that is JavaScript, unchanged. What you’re learning here is a notation for describing values.

The mental model: types describe the shape of values

Section titled “The mental model: types describe the shape of values”

The single most useful way to think about a TypeScript type is: a type is the set of values that are allowed here.

  • number is the set of all numbers.
  • 'red' is a set with exactly one member: the string "red".
  • 'red' | 'blue' is a set with two members.
  • { name: string } is the set of all objects that have at least a name property holding a string.
  • never is the empty set. unknown is the set of everything.

Assignability is subset-checking. 'red' is assignable to string because the one-element set is a subset of all strings. string is not assignable to 'red' because most strings aren’t "red".

let colour: 'red' | 'blue' = 'red';
let text: string = colour; // fine: narrower set into wider set
colour = text; // error: string is not assignable to '"red" | "blue"'

The second load-bearing idea is structural typing. TypeScript compares types by their members, not by their names. If a value has the right shape, it fits — no implements declaration needed.

interface Point { x: number; y: number }
function distance(p: Point): number {
return Math.hypot(p.x, p.y);
}
// Never mentions `Point`, but has the right shape. Accepted.
const location = { x: 3, y: 4, label: 'home' };
distance(location); // => 5

This is often called “duck typing”: if it walks like a duck, it’s a duck. It is the reason TypeScript feels natural on top of an untyped language — real JavaScript code passes around plain object literals, and structural typing accepts them.

tsc is the official compiler. It type-checks and emits JavaScript.

Terminal window
npm install --save-dev typescript
npx tsc --init # creates tsconfig.json
npx tsc # compile the project per tsconfig.json
npx tsc --noEmit # type-check only, produce no files
npx tsc --watch # recheck on save

--noEmit is the common mode in modern projects: a bundler or runtime handles the JavaScript, and tsc is used purely as a checker in your editor and CI.

tsc doesn’t execute anything. To just run a .ts file, use tsx, which transpiles on the fly:

Terminal window
npm install --save-dev tsx
npx tsx script.ts
npx tsx watch server.ts

Node.js can also do this natively: node --experimental-strip-types file.ts from Node 22.6, and type stripping is on by default from Node 23.6. It strips, it does not check, and it rejects the non-erasable constructs listed above.

typescriptlang.org/play runs the compiler in your browser. It shows the emitted JavaScript, the errors, and the inferred types on hover, with every compiler option toggleable. It is the fastest way to answer “what type does this actually have?” — use it constantly while learning.

cart.ts
type Currency = 'USD' | 'EUR';
interface LineItem {
sku: string;
quantity: number;
unitPrice: number;
}
interface Cart {
currency: Currency;
items: LineItem[];
/** Optional — may be absent. */
discountCode?: string;
}
function subtotal(cart: Cart): number {
return cart.items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
}
const cart: Cart = {
currency: 'USD',
items: [
{ sku: 'A-1', quantity: 2, unitPrice: 9.99 },
{ sku: 'B-7', quantity: 1, unitPrice: 24.5 },
],
};
console.log(subtotal(cart)); // => 44.48

Note what is not annotated: sum and item inside reduce are inferred from cart.items and the initial value 0. Good TypeScript annotates boundaries (function signatures, exported values) and lets inference handle the interior.

  • Errors move left. Typos, wrong argument order, forgotten null checks, and renamed fields become red squiggles instead of production incidents.
  • Refactoring becomes mechanical. Rename a property and the compiler lists every site that must change. Without types, that’s a grep and a prayer.
  • The types are the documentation — and unlike comments, they can’t drift out of date silently.
  • Autocomplete gets real. Your editor knows exactly what a value can do.

The cost is real too: a build step, occasional fights with the checker, and library type definitions of varying quality. For anything beyond a script, the trade is overwhelmingly worth it.

  1. Basic types — primitives, arrays, tuples, any/unknown/never, inference.
  2. Functions and objects — signatures, overloads, interface vs type.
  3. Unions and narrowing — the technique that makes the type system feel smart.
  4. Generics — types with parameters.
  5. Advanced types — conditional, mapped, and template literal types.
  6. Classes and declarations — OOP features and .d.ts files.
  7. tsconfig and tooling — configuring the compiler.
  8. Patterns and gotchas — real-world recipes and common errors.
  • TypeScript checks at compile time and emits plain JavaScript; types are erased.
  • Types cannot be inspected at runtime and do not validate incoming data.
  • Every .js file is valid TypeScript; adoption is incremental.
  • A type is the set of values allowed at a position; assignability is subset-checking.
  • TypeScript is structural — shape matters, names don’t.
  • tsc checks and compiles; tsx and Node’s type stripping run without checking.