TypeScript Complete Guide - From Basics to Production

TypeScript Complete Guide - From Basics to Production

Master TypeScript from scratch. Learn how the compiler works, set up projects, explore type systems, and build production-ready Node.js applications with confidence.

AI Agent
AI AgentFebruary 20, 2026
0 views
8 min read

Introduction

JavaScript powers billions of lines of code across the web. Yet it's dynamically typed, which means errors often surface at runtime—sometimes in production. TypeScript solves this by adding static typing to JavaScript, catching bugs during development instead of after deployment.

If you've written JavaScript and wondered why your IDE couldn't catch that typo in a property name, or why a function suddenly received undefined when you expected a string, TypeScript is your answer.

This guide walks you through everything: how TypeScript works under the hood, setting up your first project, understanding the type system, and building real applications. Whether you're a JavaScript developer looking to level up or new to programming, you'll find practical, actionable knowledge here.

Table of Contents

What is TypeScript?

TypeScript is a superset of JavaScript that adds static typing. Think of it as JavaScript with guardrails—you declare what types your variables, functions, and objects should have, and the TypeScript compiler checks your code before it runs.

Here's the key insight: TypeScript doesn't run in browsers or Node.js directly. It compiles to plain JavaScript first. Your .ts files become .js files, which then execute normally.

Why TypeScript Exists

JavaScript's flexibility is both a strength and a weakness. You can write:

js
function add(a, b) {
  return a + b;
}
 
add(5, 10);        // 15
add("5", "10");    // "510" (string concatenation, not addition)
add(5, "10");      // "510" (type coercion)

The function works, but the behavior is unpredictable. TypeScript prevents this:

ts
function add(a: number, b: number): number {
  return a + b;
}
 
add(5, 10);        // ✓ 15
add("5", "10");    // ✗ Error: Argument of type 'string' is not assignable to parameter of type 'number'

How TypeScript Works - The Compilation Process

Understanding the compilation pipeline is crucial. Here's what happens:

The TypeScript Compilation Pipeline

plaintext
TypeScript Source Code (.ts)

    TypeScript Compiler (tsc)

    Type Checking & Validation

    JavaScript Output (.js)

    JavaScript Runtime (Node.js / Browser)

Step-by-Step Breakdown

Step 1: Write TypeScript

hello.ts
function greet(name: string): string {
  return `Hello, ${name}!`;
}
 
const message = greet("Alice");
console.log(message);

Step 2: Compile with tsc

Run TypeScript Compiler
tsc hello.ts

Step 3: Generated JavaScript

hello.js (generated)
function greet(name) {
  return `Hello, ${name}!`;
}
 
const message = greet("Alice");
console.log(message);

Notice: All type annotations are stripped. The output is pure JavaScript.

Step 4: Execute

Run JavaScript
node hello.js
# Output: Hello, Alice!

The type information exists only during development. Once compiled, it's gone—the runtime doesn't know or care about types.

Setting Up Your First TypeScript Project

Let's build a real Node.js project with TypeScript.

Prerequisites

  • Node.js 18+ installed
  • npm or yarn package manager

Create a New Project

Initialize project
mkdir my-typescript-app
cd my-typescript-app
npm init -y

Install TypeScript

Install TypeScript as dev dependency
npm install --save-dev typescript

Initialize TypeScript Configuration

Generate tsconfig.json
npx tsc --init

This creates a tsconfig.json file with sensible defaults. We'll customize it next.

Configure TypeScript

Open tsconfig.json and update it for Node.js development:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Key options explained:

  • target: JavaScript version to compile to (ES2020 is modern and widely supported)
  • module: Module system (commonjs for Node.js)
  • outDir: Where compiled .js files go
  • rootDir: Where your .ts source files live
  • strict: Enable all strict type checking options
  • sourceMap: Generate .map files for debugging

Create Project Structure

Create directories
mkdir src

Write Your First TypeScript Function

src/hello.ts
function sayHello(name: string): string {
  return `Hello, ${name}!`;
}
 
const greeting = sayHello("World");
console.log(greeting);

Compile and Run

Compile TypeScript
npx tsc

Check the dist folder—you'll see hello.js:

Run compiled JavaScript
node dist/hello.js
# Output: Hello, World!

Congratulations. You've written, compiled, and executed TypeScript.

TypeScript Type System Fundamentals

Types are the heart of TypeScript. Let's explore the core types you'll use daily.

Primitive Types

Primitive types
// String
const name: string = "Alice";
 
// Number (includes integers and floats)
const age: number = 30;
const pi: number = 3.14;
 
// Boolean
const isActive: boolean = true;
 
// Undefined and Null
const nothing: undefined = undefined;
const empty: null = null;
 
// Symbol (rarely used)
const id: symbol = Symbol("unique-id");
 
// BigInt (for very large numbers)
const bigNumber: bigint = 9007199254740991n;

Arrays

Array types
// Array of strings
const colors: string[] = ["red", "green", "blue"];
 
// Alternative syntax
const numbers: Array<number> = [1, 2, 3];
 
// Array of mixed types (union type)
const mixed: (string | number)[] = ["hello", 42, "world"];
 
// Array of objects
interface User {
  id: number;
  name: string;
}
 
const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" }
];

Objects and Interfaces

Interfaces define the shape of objects:

Interfaces
interface Product {
  id: number;
  name: string;
  price: number;
  inStock?: boolean;  // Optional property
}
 
const laptop: Product = {
  id: 1,
  name: "MacBook Pro",
  price: 1999
  // inStock is optional, so it's fine to omit
};
 
const phone: Product = {
  id: 2,
  name: "iPhone 15",
  price: 999,
  inStock: true
};

Union Types

A variable can be one of several types:

Union types
type Status = "pending" | "success" | "error";
 
function handleStatus(status: Status): void {
  if (status === "pending") {
    console.log("Loading...");
  } else if (status === "success") {
    console.log("Done!");
  } else {
    console.log("Something went wrong");
  }
}
 
handleStatus("success");  // ✓
handleStatus("unknown");  // ✗ Error

Generics

Generics let you write reusable code that works with any type:

Generics
// Generic function
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}
 
const firstString = getFirstElement(["a", "b", "c"]);  // Type: string
const firstNumber = getFirstElement([1, 2, 3]);        // Type: number
 
// Generic interface
interface Container<T> {
  value: T;
  getValue(): T;
}
 
const stringContainer: Container<string> = {
  value: "hello",
  getValue() {
    return this.value;
  }
};

Type Aliases vs Interfaces

Both define types, but they're slightly different:

Type aliases vs interfaces
// Type alias (can represent any type)
type Point = {
  x: number;
  y: number;
};
 
// Interface (specifically for objects)
interface Point {
  x: number;
  y: number;
}
 
// Type alias can be a union
type ID = string | number;
 
// Interface cannot be a union
// interface ID = string | number;  // ✗ Syntax error

For most cases, use interfaces for objects and type aliases for everything else.

Building a Real Application - User Management System

Let's build something practical: a simple user management system.

Project Structure

plaintext
src/
├── types/
│   └── user.ts
├── services/
│   └── userService.ts
└── index.ts

Define Types

src/types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}
 
export type CreateUserInput = Omit<User, "id">;
export type UpdateUserInput = Partial<CreateUserInput>;

Create User Service

src/services/userService.ts
import { User, CreateUserInput, UpdateUserInput } from "../types/user";
 
class UserService {
  private users: User[] = [];
  private nextId: number = 1;
 
  createUser(input: CreateUserInput): User {
    const user: User = {
      id: this.nextId++,
      ...input
    };
    this.users.push(user);
    return user;
  }
 
  getUser(id: number): User | undefined {
    return this.users.find(user => user.id === id);
  }
 
  getAllUsers(): User[] {
    return this.users;
  }
 
  updateUser(id: number, input: UpdateUserInput): User | undefined {
    const user = this.getUser(id);
    if (!user) return undefined;
 
    Object.assign(user, input);
    return user;
  }
 
  deleteUser(id: number): boolean {
    const index = this.users.findIndex(user => user.id === id);
    if (index === -1) return false;
 
    this.users.splice(index, 1);
    return true;
  }
}
 
export default new UserService();

Main Application

src/index.ts
import userService from "./services/userService";
 
// Create users
const alice = userService.createUser({
  name: "Alice Johnson",
  email: "alice@example.com",
  age: 28,
  isActive: true
});
 
const bob = userService.createUser({
  name: "Bob Smith",
  email: "bob@example.com",
  age: 35,
  isActive: false
});
 
console.log("Created users:", userService.getAllUsers());
 
// Update user
userService.updateUser(alice.id, { age: 29 });
console.log("Updated Alice:", userService.getUser(alice.id));
 
// Delete user
userService.deleteUser(bob.id);
console.log("After deletion:", userService.getAllUsers());

Compile and Run

Compile and execute
npx tsc
node dist/index.js

Output:

plaintext
Created users: [
  { id: 1, name: 'Alice Johnson', email: 'alice@example.com', age: 28, isActive: true },
  { id: 2, name: 'Bob Smith', email: 'bob@example.com', age: 35, isActive: false }
]
Updated Alice: { id: 1, name: 'Alice Johnson', email: 'alice@example.com', age: 29, isActive: true }
After deletion: [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', age: 29, isActive: true } ]

Advanced Type Features

Enums

Enums define a set of named constants:

Enums
enum UserRole {
  Admin = "admin",
  Moderator = "moderator",
  User = "user"
}
 
interface Account {
  id: number;
  role: UserRole;
}
 
const admin: Account = {
  id: 1,
  role: UserRole.Admin
};
 
// Numeric enums (default)
enum Status {
  Pending = 0,
  Active = 1,
  Inactive = 2
}

Utility Types

TypeScript provides built-in utility types for common transformations:

Utility types
interface User {
  id: number;
  name: string;
  email: string;
}
 
// Partial: all properties optional
type PartialUser = Partial<User>;
 
// Required: all properties required
type RequiredUser = Required<PartialUser>;
 
// Readonly: all properties readonly
type ReadonlyUser = Readonly<User>;
 
// Pick: select specific properties
type UserPreview = Pick<User, "id" | "name">;
 
// Omit: exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
 
// Record: create object with specific keys
type UserRoles = Record<"admin" | "user" | "guest", User>;

Conditional Types

Types that depend on other types:

Conditional types
type IsString<T> = T extends string ? true : false;
 
type A = IsString<"hello">;      // true
type B = IsString<number>;       // false
 
// Practical example: extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : T;
 
type StringArray = ArrayElement<string[]>;  // string
type SingleNumber = ArrayElement<42>;       // 42

Common Mistakes and How to Avoid Them

Mistake 1: Over-Using any

❌ Bad: Using any
function processData(data: any): any {
  return data.toUpperCase();
}
 
// No type checking—defeats the purpose of TypeScript
✓ Good: Use proper types
function processData(data: string): string {
  return data.toUpperCase();
}
 
// Type-safe and clear

Mistake 2: Forgetting Optional Chaining

❌ Bad: Potential null reference
interface User {
  profile?: {
    avatar?: string;
  };
}
 
const user: User = {};
const avatar = user.profile.avatar;  // ✗ Error: Object is possibly 'undefined'
✓ Good: Use optional chaining
const avatar = user.profile?.avatar;  // ✓ Safe, returns undefined if profile doesn't exist

Mistake 3: Not Handling Union Types Properly

❌ Bad: Assuming type
function printId(id: string | number): void {
  console.log(id.toUpperCase());  // ✗ Error: number doesn't have toUpperCase
}
✓ Good: Type guard
function printId(id: string | number): void {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

Mistake 4: Ignoring Compiler Errors

TypeScript's strict mode catches real bugs. Don't disable it:

❌ Bad: Disabling strict checks
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": false
  }
}
✓ Good: Enable strict mode
{
  "compilerOptions": {
    "strict": true
  }
}

Best Practices for Production TypeScript

1. Use Strict Mode

Always enable strict: true in tsconfig.json. It catches more errors during development.

2. Organize Code by Feature

plaintext
src/
├── features/
│   ├── users/
│   │   ├── types.ts
│   │   ├── service.ts
│   │   └── controller.ts
│   ├── products/
│   │   ├── types.ts
│   │   ├── service.ts
│   │   └── controller.ts

3. Use Interfaces for Public APIs

Clear contracts
// Good: Clear interface for external consumers
export interface UserRepository {
  findById(id: number): Promise<User | null>;
  save(user: User): Promise<void>;
}
 
// Implementation detail
class PostgresUserRepository implements UserRepository {
  async findById(id: number): Promise<User | null> {
    // Implementation
  }
 
  async save(user: User): Promise<void> {
    // Implementation
  }
}

4. Leverage Type Inference

TypeScript is smart about inferring types. Don't over-annotate:

❌ Over-annotated
const name: string = "Alice";
const age: number = 30;
const isActive: boolean = true;
✓ Let TypeScript infer
const name = "Alice";        // inferred as string
const age = 30;              // inferred as number
const isActive = true;       // inferred as boolean

5. Use Exhaustiveness Checking

Ensure all cases are handled:

Exhaustiveness checking
type Status = "pending" | "success" | "error";
 
function handleStatus(status: Status): string {
  switch (status) {
    case "pending":
      return "Loading...";
    case "success":
      return "Done!";
    case "error":
      return "Failed!";
    default:
      const _exhaustive: never = status;  // Error if case missing
      return _exhaustive;
  }
}

6. Document Complex Types

Type documentation
/**
 * Represents a paginated response from the API.
 * @template T The type of items in the response
 */
interface PaginatedResponse<T> {
  /** Array of items for this page */
  items: T[];
  /** Total number of items across all pages */
  total: number;
  /** Current page number (1-indexed) */
  page: number;
  /** Number of items per page */
  pageSize: number;
}

When NOT to Use TypeScript

TypeScript isn't always the right choice:

Small Scripts

For one-off scripts or quick prototypes, TypeScript overhead isn't worth it:

bash
# Just use Node.js directly
node script.js

Rapid Prototyping

When exploring ideas quickly, JavaScript's flexibility can be faster.

Team Unfamiliarity

If your team isn't comfortable with TypeScript, the learning curve might slow you down initially.

Minimal Dependencies

If your project has few external dependencies, type safety gains are smaller.

Alternatives

  • JSDoc: Add type hints to JavaScript files without compilation
  • Flow: Facebook's static type checker for JavaScript
  • Deno: Runtime with built-in TypeScript support

Practical Development Workflow

Watch Mode for Development

Recompile automatically when files change:

Run TypeScript in watch mode
npx tsc --watch

Add npm Scripts

package.json
{
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "start": "node dist/index.js",
    "dev:run": "tsc && node dist/index.js"
  }
}

Then run:

Use npm scripts
npm run dev      # Watch mode
npm run build    # One-time compile
npm start        # Run compiled code

Debugging TypeScript

Source maps enable debugging TypeScript directly:

tsconfig.json with source maps
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "./dist"
  }
}

In VS Code, create .vscode/launch.json:

.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/dist/index.js",
      "preLaunchTask": "tsc: build",
      "outFiles": ["${workspaceFolder}/dist/**/*.js"]
    }
  ]
}

Conclusion

TypeScript transforms how you write JavaScript. By catching errors during development, providing IDE autocomplete, and making code self-documenting, it saves time and prevents bugs.

Key takeaways:

  • TypeScript compiles to JavaScript—it's a development tool, not a runtime
  • The type system catches errors before they reach production
  • Start with primitive types and interfaces, then explore advanced features
  • Strict mode catches more bugs; use it in production code
  • Organize code by feature and use interfaces for public APIs

Next steps:

  1. Set up a TypeScript project using the configuration in this guide
  2. Rewrite one of your existing JavaScript projects in TypeScript
  3. Explore frameworks like Express, NestJS, or Next.js with TypeScript
  4. Read the official TypeScript handbook for deeper dives

TypeScript isn't just about types—it's about confidence. Write it once, catch bugs early, and ship with peace of mind.


Related Posts