Programming Language

TypeScript

TypeScript adds static types to JavaScript, catching bugs before they reach production. Here's what it is, how it works, and how to start using it today.

beginner20 min readUpdated Sep 15, 2026
typescript
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const message = greet("TypeScript");
console.log(message);
Created
2012, by Microsoft
Type
Statically typed superset of JavaScript
Compiles to
Plain JavaScript
Standard
ECMAScript with type extensions
File extension
.ts / .tsx
Current version
TypeScript 5.x

Why it matters

Why TypeScript matters

Catch bugs early

Type errors are caught at compile time, not in production. You find mistakes before your code ever runs.

Better editor experience

Autocomplete, inline documentation and refactoring tools work because the editor understands your code's shape.

Self-documenting code

Types describe what functions expect and return. Reading the signature tells you exactly how to use it.

The big picture

The three layers of TypeScript

TypeScript is a language, a compiler and a type system. Understanding which layer you are working in makes debugging far easier.

TypeScript

The language

JavaScript plus type annotations, interfaces, generics and advanced type operations.

The compiler

tsc

Converts TypeScript to JavaScript, checks types and emits clean output for any target.

The ecosystem

Tools & libraries

IDE support, declaration files, strict mode and frameworks that ship with TypeScript built in.

TypeScript at a glance

What the language gives you

Primitive types

string, number, boolean, null, undefined, symbol, bigint.

Object types

Interfaces, type aliases, classes and arrays.

Unions & intersections

Combine types with | (or) and & (and) to model flexible data.

Type narrowing

Use typeof, in and discriminants to let TypeScript narrow types inside conditionals.

Generics

Write reusable functions and types that work with any type while staying type-safe.

Utility types

Built-in transforms like Partial, Pick and Record that modify existing types.

A short history

From optional annotations to industry standard

  1. 2012

    TypeScript launches

    Microsoft releases TypeScript as an optional type layer for JavaScript.

    12
  2. 2016

    Angular bets on TypeScript

    Angular adopts TypeScript as its primary language, pushing adoption across the ecosystem.

    16
  3. 2018

    TypeScript 3.0

    Project references, unknown type and tuples make the language more powerful.

    18
  4. 2020

    Variadic tuples

    TypeScript 4.0 brings variadic tuple types and labeled tuple elements.

    20
  5. 2023

    Decorators standardize

    TC39 decorators land in TypeScript 5.0, aligning with the ECMAScript proposal.

    23
  6. Today

    The default choice

    Most new JavaScript projects start with TypeScript. Frameworks ship with it built in.

    Today

The complete guide

TypeScript: Everything you need to know

What is TypeScript?

TypeScript is a statically typed superset of JavaScript that adds optional type annotations checked at compile time. It compiles to plain JavaScript, so it runs anywhere JavaScript runs — browsers, Node.js, Deno and Bun.

Created by Microsoft in 2012, TypeScript has become the default choice for serious JavaScript projects. The reason is simple: it catches bugs before your code runs. A typo in a property name, a missing argument, a wrong return type — TypeScript flags these at compile time instead of letting them become production crashes.

The best way to think about TypeScript is as JavaScript with a safety net. You write the same language you already know, but you can describe the shape of your data and the contracts of your functions. The compiler checks that everything fits, and then strips the types away, leaving clean JavaScript.

Why TypeScript matters

JavaScript is dynamically typed, which means you can pass any value to any function and the error only shows up when that code actually runs. In a small script that is fine. In a large application with dozens of developers, it is a recipe for production bugs.

TypeScript solves this by checking types before the code runs:

  • Catch bugs at compile time — misspell a property, pass the wrong argument type or forget to handle null, and TypeScript tells you immediately.
  • Better editor experience — autocomplete works because the editor knows what properties an object has. Refactoring is safe because the compiler finds every place that needs updating.
  • Self-documenting code — function signatures describe exactly what they expect and return. Reading the types tells you how to use the code without reading the implementation.
  • Safer refactoring — change a type and the compiler highlights every place that breaks. No more hunting through the codebase for runtime errors.
  • Progressive adoption — you can add TypeScript to an existing JavaScript project incrementally, one file at a time.

TypeScript does not add runtime overhead. The types are erased during compilation, so the JavaScript that ships to users is identical whether you wrote it in TypeScript or not.

How TypeScript works

TypeScript code lives in .ts files. The TypeScript compiler (tsc) reads these files, checks the types, and outputs .js files with the type annotations removed.

// app.ts
function add(a: number, b: number): number {
  return a + b;
}

const result = add(1, 2); // result is number

The compiler checks that a and b are numbers, that the function returns a number, and that result receives a number. If anything does not match, you get a compile-time error. The output JavaScript is simply:

function add(a, b) {
  return a + b;
}
const result = add(1, 2);

No types, no overhead. The safety was in the build step only.

Type annotations

Type annotations describe the type of a variable, parameter or return value:

// primitives
let name: string = "Ada";
let age: number = 36;
let active: boolean = true;

// arrays
let skills: string[] = ["HTML", "CSS"];
let scores: Array<number> = [95, 87, 92];

// function parameters and return
function greet(name: string): string {
  return `Hello, ${name}!`;
}

You do not always need annotations. TypeScript infers types from the value:

let count = 0;          // inferred as number
const items = ["a"];    // inferred as string[]

Prefer inference when the type is obvious. Annotate when the type is not clear from the value, when you need a more general type, or when the default inference is too narrow.

Type inference

TypeScript automatically deduces types from context:

// from assignment
let x = 5; // number

// from return value
function doubling(n: number) {
  return n * 2; // inferred as number
}

// from context
const numbers = [1, 2, 3]; // number[]
const doubled = numbers.map(n => n * 2); // number[]

Inference means you write less code while getting the same safety. Most TypeScript code relies heavily on inference — only annotate when it adds clarity.

Union types

A union type says a value can be one of several types:

type Status = "loading" | "success" | "error";

function handleStatus(status: Status) {
  if (status === "loading") {
    // TypeScript knows status is "loading" here
  }
}

let value: string | number;
value = "hello";
value = 42;

Unions model real-world data well. API responses might succeed or fail. A field might be a string or null. A function might accept different input types.

Type narrowing

TypeScript narrows union types inside conditionals:

function process(value: string | number) {
  if (typeof value === "string") {
    // TypeScript knows value is string here
    return value.toUpperCase();
  }
  // TypeScript knows value is number here
  return value.toFixed(2);
}

Narrowing also works with:

  • in operator — check if a property exists
  • instanceof — check class instances
  • Equality checks — === and !==
  • Discriminated unions — check a common tag property
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;

function area(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
  }
}

Discriminated unions are one of TypeScript’s most powerful patterns. The kind property tells TypeScript exactly which variant you are dealing with.

Interfaces and type aliases

An interface describes the shape of an object — its properties, their types and whether they are required. A type alias gives any type a name, including unions, tuples, primitives and function types.

// Interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Type alias with the same shape
type User = {
  id: number;
  name: string;
  email: string;
};

For plain object shapes the two are interchangeable. The differences appear when you compose or extend types.

Optional and readonly properties

interface User {
  id: number;
  name: string;
  email?: string;      // optional — string | undefined
  readonly createdAt: Date;
}

const user: User = { id: 1, name: "Ada", createdAt: new Date() };
user.createdAt = new Date(); // Error: cannot assign to a readonly property

Extending interfaces

Interfaces inherit from other interfaces with extends, including several at once:

interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface User extends Timestamped {
  id: number;
  name: string;
}

Type aliases for unions, tuples and functions

Type aliases shine when the type is not a plain object:

type ID = string | number;
type Status = "idle" | "loading" | "success" | "error";
type Pair = [string, number];
type Callback = (data: unknown) => void;

Intersection types

Combine types with &:

type User = { id: number; name: string };
type WithTimestamp = { createdAt: Date };

type TimestampedUser = User & WithTimestamp;

Interface or type?

Use interfaces for object shapes that may be extended or implemented by classes. Use type aliases for unions, intersections, tuples and function types. Pick one convention for object shapes and stay consistent.

Generics

Generics are type parameters: they let a function, interface or class work with any type while preserving type safety. Instead of hardcoding a type or falling back to any, you pass a type variable that is filled in at the call site.

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const num = first([1, 2, 3]);    // number
const str = first(["a", "b"]);   // string

Without generics you would either use any and lose type information, or write a separate function for every type.

Generic functions

A type parameter is declared in angle brackets. TypeScript usually infers it from the arguments, so you rarely pass it explicitly:

function merge<T, U>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const result = merge({ name: "Ada" }, { age: 36 });
// { name: string; age: number }

Generic interfaces and classes

interface Response<T> {
  data: T;
  status: number;
}

interface User {
  id: number;
  name: string;
}

const response: Response<User> = {
  data: { id: 1, name: "Ada" },
  status: 200,
};

class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

Constraints

Use extends to restrict what a generic can accept:

interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find((item) => item.id === id);
}

Default type parameters

interface Paginated<T = unknown> {
  data: T[];
  total: number;
  page: number;
}

const response: Paginated = { data: [1, "a"], total: 10, page: 1 };

Generics are erased at runtime, so they add no overhead — they exist only to make the compiler and your editor smarter.

The tsconfig.json

The tsconfig.json file configures the TypeScript compiler:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Key options:

  • target — which JavaScript version to emit (ES2020 is a good default)
  • strict — enables all strict type-checking flags
  • module — the module system for output (ESNext for modern projects)
  • outDir — where compiled JavaScript goes
  • esModuleInterop — allows default imports from CommonJS modules

Always use "strict": true for new projects. It catches more bugs at the cost of requiring slightly more explicit code.

Installing TypeScript

# As a project dependency (recommended)
npm install --save-dev typescript

# Globally (not recommended for teams)
npm install -g typescript

Compile your project:

# Using npx
npx tsc

# Or add a script to package.json
npm run build

For development with watch mode:

npx tsc --watch

Most modern frameworks (Next.js, Vite, Astro) handle TypeScript compilation for you — you do not need to run tsc manually.

Common mistakes

  • Using any everywhere, which disables type checking.
  • Annotating everything when inference already knows the type.
  • Not enabling strict mode in tsconfig.
  • Confusing TypeScript types with runtime values — types are erased at compile time.
  • Importing types incorrectly — use import type for type-only imports.
  • Ignoring compiler errors instead of fixing them.
  • Not installing @types packages for JavaScript libraries.
  • Making every interface property optional, which hides required checks.
  • Using any instead of a generic, throwing away the type information you wanted to keep.

Best practices

  • Enable strict mode from day one.
  • Let inference do the work — only annotate when it adds value.
  • Use unknown instead of any when you do not know the type.
  • Prefer interfaces for object shapes that might be extended.
  • Use discriminated unions for state machines and variant data.
  • Keep type definitions close to where they are used.
  • Use import type for type-only imports to keep runtime code small.
  • Treat the compiler as a teammate, not an obstacle.

What to learn next

You now have the TypeScript fundamentals: annotations, inference, unions, narrowing, interfaces, type aliases and generics. From here the natural next step is Advanced Types for utility types, conditional types and mapped types. Pick a project, refactor some code and let the practice compound.

Type annotations

Explicit annotations help when the type is not obvious. Let inference do the work when it is.

Prefer
const count = 0;
const items: string[] = [];

function add(a: number, b: number) {
  return a + b;
}
Avoid
const count: number = 0;
const items: Array<string> = [];

function add(a: number, b: number): number {
  return a + b;
}

Using any

any disables type checking. Use unknown when you do not know the type and narrow it.

Prefer
function parse(input: unknown) {
  if (typeof input === "string") {
    return input.toUpperCase();
  }
  return String(input);
}
Avoid
function parse(input: any) {
  return input.toUpperCase();
}

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning TypeScript?

Our interactive tutorial walks you through TypeScript step by step — with quizzes and real code you can run in the browser.