Why TypeScript? Real Bugs Caught by the Compiler

If you have ever shipped JavaScript code to production only to discover a Cannot read property of undefined error at 2 AM, TypeScript is your answer. TypeScript is a statically typed superset of JavaScript that compiles down to plain JavaScript. It catches entire categories of bugs before your code ever runs.

Real Bug Examples TypeScript Prevents

Consider this perfectly valid JavaScript that silently fails:

// JavaScript — no errors, but breaks at runtime
function getUser(id) {
  return fetch(`/api/users/${id}`);
}

// Typo in property name — JS says nothing
const user = await getUser(1);
console.log(user.nmae); // undefined — no error!

// Wrong argument type — JS says nothing
getUser("not-a-number"); // API call with bad data

// Missing function arguments
function calculateTotal(price, quantity, tax) {
  return price * quantity * (1 + tax);
}
calculateTotal(10, 5); // NaN — tax is undefined

TypeScript catches every single one of these at compile time:

// TypeScript — errors caught BEFORE runtime
interface User {
  id: number;
  name: string;
  email: string;
}

async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

const user = await getUser(1);
console.log(user.nmae);
// TS2551: Property 'nmae' does not exist on type 'User'. Did you mean 'name'?

getUser("not-a-number");
// TS2345: Argument of type 'string' is not assignable to parameter of type 'number'

function calculateTotal(price: number, quantity: number, tax: number): number {
  return price * quantity * (1 + tax);
}
calculateTotal(10, 5);
// TS2554: Expected 3 arguments, but got 2

Installation and Project Setup

Installing TypeScript

# Install globally
npm install -g typescript

# Or install as a dev dependency (recommended)
npm install --save-dev typescript

# Verify installation
tsc --version

# Initialize a TypeScript project
tsc --init

tsconfig.json Configuration Explained Field by Field

The tsconfig.json file controls how TypeScript compiles your code. Here is a production-ready configuration with every important field explained:

{
  "compilerOptions": {
    // === Output Settings ===
    "target": "ES2022",          // JS version to compile to (ES5, ES6, ES2020, ES2022, ESNext)
    "module": "NodeNext",        // Module system (CommonJS, ESNext, NodeNext)
    "outDir": "./dist",          // Where compiled JS files go
    "rootDir": "./src",          // Where your TS source files live
    "declaration": true,         // Generate .d.ts declaration files
    "sourceMap": true,           // Generate source maps for debugging

    // === Strictness (turn ALL on for new projects) ===
    "strict": true,              // Enables ALL strict checks below:
    "noImplicitAny": true,       // Error on implicit 'any' type
    "strictNullChecks": true,    // null/undefined are distinct types
    "strictFunctionTypes": true, // Strict checking of function types
    "strictBindCallApply": true, // Strict bind, call, apply methods
    "noImplicitThis": true,      // Error on 'this' with implicit 'any' type
    "alwaysStrict": true,        // Emit "use strict" in every file

    // === Module Resolution ===
    "moduleResolution": "NodeNext",  // How TS finds modules
    "esModuleInterop": true,         // Better CommonJS/ESM interop
    "resolveJsonModule": true,       // Allow importing .json files
    "allowSyntheticDefaultImports": true,

    // === Quality Checks ===
    "noUnusedLocals": true,          // Error on unused variables
    "noUnusedParameters": true,      // Error on unused parameters
    "noImplicitReturns": true,       // Error if not all paths return
    "noFallthroughCasesInSwitch": true, // Error on switch fallthrough
    "forceConsistentCasingInFileNames": true, // Enforce file name casing

    // === Path Aliases ===
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Basic Types: The Foundation

Primitive Types

// String, Number, Boolean
let username: string = "Alice";
let age: number = 30;
let isActive: boolean = true;

// Arrays
let scores: number[] = [95, 87, 92];
let names: Array<string> = ["Alice", "Bob"];

// Tuples — fixed-length arrays with specific types per position
let coordinates: [number, number] = [40.7128, -74.0060];
let userEntry: [string, number, boolean] = ["Alice", 30, true];

// Enums — named constants
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

enum HttpStatus {
  OK = 200,
  NotFound = 404,
  InternalError = 500
}

const move = Direction.Up;  // "UP"
const status = HttpStatus.OK; // 200

// Any, Unknown, Never, Void
let flexible: any = "can be anything"; // Avoid — disables type checking
let safe: unknown = "must be checked before use";
function throwError(msg: string): never { throw new Error(msg); }
function logMessage(msg: string): void { console.log(msg); }

// Type assertions
const input = document.getElementById("email") as HTMLInputElement;
input.value = "user@example.com";

Interfaces vs Types: When to Use Which

Both interface and type define object shapes, but they have key differences:

// INTERFACE — best for object shapes, can be extended/merged
interface User {
  id: number;
  name: string;
  email: string;
}

// Extending an interface
interface Admin extends User {
  permissions: string[];
}

// Declaration merging (only interfaces can do this)
interface User {
  avatar?: string; // This MERGES with the original User interface
}

// TYPE — best for unions, intersections, primitives, utility types
type ID = string | number;

type Status = "active" | "inactive" | "banned";

type Point = {
  x: number;
  y: number;
};

// Intersection types
type AdminUser = User & { permissions: string[] };

// Mapped types (only type aliases can do this)
type ReadonlyUser = Readonly<User>;

// Conditional types
type IsString<T> = T extends string ? true : false;

Decision Guide

Use Interface WhenUse Type When
Defining object shapesCreating union types
Class contracts (implements)Creating intersection types
Library/API public typesAliasing primitive types
You need declaration mergingUsing mapped/conditional types
Extending other interfacesDefining tuple types

Generics: Write Reusable, Type-Safe Code

Generic API Client

// A type-safe API client using generics
class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get<T>(endpoint: string): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return response.json() as Promise<T>;
  }

  async post<TBody, TResponse>(endpoint: string, body: TBody): Promise<TResponse> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    return response.json() as Promise<TResponse>;
  }
}

// Usage — fully type-safe
interface User { id: number; name: string; email: string; }
interface CreateUserDTO { name: string; email: string; }

const api = new ApiClient("https://api.example.com");

const user = await api.get<User>("/users/1");
console.log(user.name); // TypeScript knows this is a string

const newUser = await api.post<CreateUserDTO, User>("/users", {
  name: "Alice",
  email: "alice@example.com"
});

Generic React Components

// Generic select dropdown component
interface SelectProps<T> {
  items: T[];
  selectedItem: T | null;
  onSelect: (item: T) => void;
  getLabel: (item: T) => string;
  getKey: (item: T) => string | number;
}

function Select<T>({ items, selectedItem, onSelect, getLabel, getKey }: SelectProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={getKey(item)} onClick={() => onSelect(item)}>
          {getLabel(item)}
        </li>
      ))}
    </ul>
  );
}

// Usage with User type — fully type-safe
<Select<User>
  items={users}
  selectedItem={currentUser}
  onSelect={(user) => setCurrentUser(user)}
  getLabel={(user) => user.name}
  getKey={(user) => user.id}
/>

// Usage with Product type — same component, different type
interface Product { sku: string; title: string; price: number; }

<Select<Product>
  items={products}
  selectedItem={null}
  onSelect={(product) => addToCart(product)}
  getLabel={(p) => `${p.title} - $${p.price}`}
  getKey={(p) => p.sku}
/>

Utility Types: Built-in Type Transformations

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Partial — all properties become optional
type UpdateUserDTO = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

// Required — all properties become required
type RequiredUser = Required<Partial<User>>;

// Pick — select specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }

// Omit — remove specific properties
type PublicUser = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date; }

// Record — create object type with specified keys and value type
type UserRoles = Record<"admin" | "editor" | "viewer", string[]>;
const roles: UserRoles = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"]
};

// Exclude — remove types from a union
type Status = "active" | "inactive" | "banned";
type ActiveStatus = Exclude<Status, "banned">; // "active" | "inactive"

// Extract — keep only matching types from a union
type StringOrNumber = Extract<string | number | boolean, string | number>;
// string | number

// ReturnType — get the return type of a function
function createUser(name: string, email: string) {
  return { id: Date.now(), name, email, createdAt: new Date() };
}
type NewUser = ReturnType<typeof createUser>;
// { id: number; name: string; email: string; createdAt: Date; }

Type Guards and Narrowing

// typeof guard
function padLeft(value: string | number) {
  if (typeof value === "number") {
    return " ".repeat(value); // TS knows value is number here
  }
  return value; // TS knows value is string here
}

// instanceof guard
function processDate(date: string | Date) {
  if (date instanceof Date) {
    return date.toISOString(); // TS knows date is Date
  }
  return new Date(date).toISOString(); // TS knows date is string
}

// Custom type guard with 'is' keyword
interface Fish { swim: () => void; }
interface Bird { fly: () => void; }

function isFish(animal: Fish | Bird): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

function moveAnimal(animal: Fish | Bird) {
  if (isFish(animal)) {
    animal.swim(); // TS knows this is Fish
  } else {
    animal.fly(); // TS knows this is Bird
  }
}

// 'in' operator guard
interface Admin { role: "admin"; permissions: string[]; }
interface RegularUser { role: "user"; subscription: string; }

function getInfo(user: Admin | RegularUser) {
  if ("permissions" in user) {
    return user.permissions; // TS knows this is Admin
  }
  return user.subscription; // TS knows this is RegularUser
}

Discriminated Unions

// A common discriminant property (kind/type/status) narrows the union
interface LoadingState {
  status: "loading";
}

interface SuccessState {
  status: "success";
  data: any[];
}

interface ErrorState {
  status: "error";
  error: string;
}

type RequestState = LoadingState | SuccessState | ErrorState;

function renderUI(state: RequestState): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return `Loaded ${state.data.length} items`; // TS knows data exists
    case "error":
      return `Error: ${state.error}`; // TS knows error exists
  }
}

// Real-world: Redux-style actions
type Action =
  | { type: "ADD_TODO"; payload: { text: string } }
  | { type: "TOGGLE_TODO"; payload: { id: number } }
  | { type: "DELETE_TODO"; payload: { id: number } };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.payload.text, done: false }];
    case "TOGGLE_TODO":
      return state.map(t => t.id === action.payload.id ? { ...t, done: !t.done } : t);
    case "DELETE_TODO":
      return state.filter(t => t.id !== action.payload.id);
  }
}

Declaration Files (.d.ts)

Declaration files provide type information for JavaScript libraries that do not include their own types.

// types/legacy-analytics.d.ts
// For a legacy JS library loaded via script tag
declare namespace Analytics {
  function track(event: string, properties?: Record<string, any>): void;
  function identify(userId: string, traits?: Record<string, any>): void;
  function page(name?: string): void;
}

// types/global.d.ts
// Augment the global Window interface
declare global {
  interface Window {
    __APP_CONFIG__: {
      apiUrl: string;
      environment: "development" | "staging" | "production";
    };
  }
}

export {};
# Most libraries have types on DefinitelyTyped
npm install --save-dev @types/node
npm install --save-dev @types/express
npm install --save-dev @types/react
npm install --save-dev @types/lodash

# Check if types exist
npx typesearch lodash

Migrating a Real JavaScript Project: Step by Step

Step 1: Install TypeScript and Initialize

npm install --save-dev typescript @types/node
npx tsc --init

Step 2: Start with allowJs (Gradual Migration)

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "allowJs": true,         // Allow JS files alongside TS files
    "checkJs": false,        // Don't type-check JS files yet
    "strict": false,         // Start relaxed, tighten later
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Step 3: Rename Files One at a Time

# Start with utility files (fewest dependencies)
mv src/utils/helpers.js src/utils/helpers.ts
mv src/utils/validators.js src/utils/validators.ts

# Then move to models/types
mv src/models/user.js src/models/user.ts

# Finally convert route handlers and main files
mv src/routes/userRoutes.js src/routes/userRoutes.ts
mv src/index.js src/index.ts

Step 4: Gradually Enable Strict Mode

{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,          // Enable first
    "strictNullChecks": false,      // Enable second
    "strictFunctionTypes": false    // Enable third
  }
}

Step 5: Fix Errors As You Go

# Check how many errors you have
npx tsc --noEmit | wc -l

# Fix errors in batches — focus on one file at a time
npx tsc --noEmit 2>&1 | grep "src/utils/helpers.ts"

TypeScript with React

// Typing functional components
import React, { useState, useRef, useEffect, ChangeEvent, FormEvent } from "react";

// Props interface
interface UserCardProps {
  user: User;
  onEdit: (id: number) => void;
  onDelete?: (id: number) => void; // optional prop
  children?: React.ReactNode;
}

// Functional component with typed props
const UserCard: React.FC<UserCardProps> = ({ user, onEdit, onDelete, children }) => {
  const [isEditing, setIsEditing] = useState<boolean>(false);
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (isEditing && inputRef.current) {
      inputRef.current.focus();
    }
  }, [isEditing]);

  // Typed event handlers
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // form logic
  };

  return (
    <div>
      <h3>{user.name}</h3>
      <input ref={inputRef} onChange={handleChange} />
      <button onClick={() => onEdit(user.id)}>Edit</button>
      {onDelete && <button onClick={() => onDelete(user.id)}>Delete</button>}
      {children}
    </div>
  );
};

TypeScript with Node.js and Express

npm install express
npm install --save-dev @types/express @types/node typescript ts-node nodemon
// src/server.ts
import express, { Request, Response, NextFunction } from "express";

interface CreateUserBody {
  name: string;
  email: string;
}

interface UserParams {
  id: string;
}

const app = express();
app.use(express.json());

// Typed route handler
app.get("/api/users/:id", (req: Request<UserParams>, res: Response) => {
  const userId = parseInt(req.params.id, 10);
  // ...
  res.json({ id: userId, name: "Alice" });
});

app.post("/api/users", (req: Request<{}, {}, CreateUserBody>, res: Response) => {
  const { name, email } = req.body; // Fully typed
  // ...
  res.status(201).json({ id: 1, name, email });
});

// Typed error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ error: err.message });
});

app.listen(3000, () => console.log("Server running on port 3000"));

Common TypeScript Errors and Fixes

TS2322: Type 'X' is not assignable to type 'Y'

Problem: You are assigning a value of the wrong type.

Cause: Type mismatch between the value and the expected type.

Solution:

// Error
let count: number = "5"; // TS2322

// Fix — use the correct type
let count: number = 5;

// Or fix — change the type annotation
let count: string = "5";

// Or fix — convert the value
let count: number = parseInt("5", 10);

TS2345: Argument of type 'X' is not assignable to parameter of type 'Y'

Problem: Passing wrong argument type to a function.

Cause: Function expects a specific type but receives another.

Solution:

function greet(name: string) { return `Hello, ${name}`; }

// Error
greet(42); // TS2345

// Fix
greet("Alice");
greet(String(42));

TS7006: Parameter 'x' implicitly has an 'any' type

Problem: Function parameter has no type annotation with noImplicitAny enabled.

Cause: TypeScript cannot infer the type.

Solution:

// Error
function add(a, b) { return a + b; } // TS7006

// Fix — add type annotations
function add(a: number, b: number): number { return a + b; }

TS2304: Cannot find name 'X'

Problem: TypeScript does not recognize a global variable or type.

Cause: Missing type declarations or imports.

Solution:

# Install missing type definitions
npm install --save-dev @types/node
npm install --save-dev @types/jest

# Or declare the module
echo 'declare module "untyped-package";' > src/types/untyped-package.d.ts

Path Aliases (@/ imports)

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@types/*": ["src/types/*"]
    }
  }
}
// Before — fragile relative imports
import { formatDate } from "../../../utils/formatDate";
import { UserCard } from "../../components/UserCard";

// After — clean absolute imports
import { formatDate } from "@utils/formatDate";
import { UserCard } from "@components/UserCard";
# For Node.js projects, also configure module-alias
npm install module-alias
# Or use tsx/ts-node with tsconfig-paths
npm install --save-dev tsconfig-paths
npx ts-node -r tsconfig-paths/register src/index.ts

Debugging TypeScript

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug TS",
      "runtimeExecutable": "npx",
      "runtimeArgs": ["ts-node", "--project", "tsconfig.json"],
      "args": ["${workspaceFolder}/src/index.ts"],
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/dist/**/*.js"]
    }
  ]
}
# Compile with source maps
tsc --sourceMap

# Use ts-node for direct execution during development
npx ts-node src/index.ts

# Use tsx for faster execution (esbuild-based)
npx tsx src/index.ts

# Watch mode
npx tsc --watch

Quick Reference Cheat Sheet

ConceptSyntaxExample
Basic typelet x: typelet name: string = "hi"
Arraytype[]let nums: number[] = [1,2]
Tuple[type, type]let t: [string, number]
UnionA | Blet id: string | number
IntersectionA & Btype C = User & Admin
Optionalprop?{ name?: string }
Readonlyreadonly prop{ readonly id: number }
Generic<T>function id<T>(x: T): T
Type assertionas Typevalue as string
Type guardis Typex is string
keyofkeyof Typetype Keys = keyof User
typeoftypeof variabletype T = typeof obj
Enumenum Name {}enum Color { Red, Blue }
PartialPartial<T>Partial<User>
PickPick<T, K>Pick<User, "name">
OmitOmit<T, K>Omit<User, "password">
RecordRecord<K, V>Record<string, number>