DEV Community

Alex Chen
Alex Chen

Posted on

TypeScript for JavaScript Developers: The Complete Practical Guide (2026)

TypeScript: The Practical Guide for JavaScript Developers (2026)

TypeScript isn't just "JavaScript with types" — it's a superpower that catches bugs before they happen. Here's the practical guide to going from JS to TS.

Why TypeScript Matters

// JavaScript: The bug that only shows in production
function calculateDiscount(price, isMember) {
  return price * (isMember ? 0.9 : 0.8); // What if price is "100"? NaN!
}

// TypeScript: Caught at compile time
function calculateDiscount(price: number, isMember: boolean): number {
  return price * (isMember ? 0.9 : 0.8);
}
calculateDiscount("100", true); // Error: Argument of type 'string' not assignable to 'number'
Enter fullscreen mode Exit fullscreen mode

Type Basics

// Primitive types
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let data: null = null;
let nothing: undefined = undefined;

// Arrays
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ["Alice", "Bob"];
// Read-only array
const config: readonly string[] = ["dev", "staging"];

// Objects
interface User {
  id: string;
  name: string;
  email: string;
  role?: "admin" | "user"; // Optional + union type
  createdAt: Date;
}

const user: User = {
  id: crypto.randomUUID(),
  name: "Alice",
  email: "alice@example.com",
  createdAt: new Date(),
};

// Functions with types
function greet(name: string): string { // Return type annotation
  return `Hello, ${name}!`;
}
// Arrow function version:
const double = (n: number): number => n * 2;

// Void for functions that don't return
function log(message: string): void {
  console.log(message);
}

// Never for functions that never complete
function fail(message: string): never {
  throw new Error(message);
}
Enter fullscreen mode Exit fullscreen mode

Interfaces vs Type Aliases

// Interface — best for object shapes (extensible)
interface Product {
  id: string;
  name: string;
  price: number;
  category?: string;
}

// Extending interfaces
interface DigitalProduct extends Product {
  downloadUrl: string;
  fileSize: number;
}

// Type alias — more flexible (unions, tuples, computed types)
type ID = string | number; // Union type
type Status = "pending" | "active" | "archived";
type Pair<T> = [T, T]; // Generic tuple

// Intersection types (combining multiple types)
type WithTimestamps<T> = T & {
  createdAt: Date;
  updatedAt: Date;
};
type TimestampedProduct = WithTimestamps<Product>;

// When to use which:
// ✅ Interface: Object shapes, class implementation, needs extending
// ✅ Type alias: Unions, intersections, tuples, mapped types, complex computed types
Enter fullscreen mode Exit fullscreen mode

Generics: Reusable Types

// Basic generic function
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}
firstElement([1, 2, 3]);       // Returns number | undefined
firstElement(["a", "b"]);      // Returns string | undefined

// Generic interface
interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
  timestamp: number;
}

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

// Generic class
class Repository<T extends { id: string }> {
  private items: Map<string, T> = new Map();

  async save(item: T): Promise<void> {
    this.items.set(item.id, item);
    await db.collection('items').doc(item.id).set(item);
  }

  async find(id: string): Promise<T | null> {
    return this.items.get(id) ?? null;
  }

  async findAll(): Promise<T[]> {
    return Array.from(this.items.values());
  }
}

// Constrained generics
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
getProperty({ name: "Alice", age: 30 }, "name"); // Returns string
getProperty({ name: "Alice", age: 30 }, "age");   // Returns number
getProperty({ name: "Alice", age: 30 }, "email"); // Error!
Enter fullscreen mode Exit fullscreen mode

Utility Types (Built-in Type Transformers)

// Partial<T> — make all properties optional
function updateProduct(id: string, fields: Partial<Product>): Product {
  const existing = db.get(id);
  return { ...existing, ...fields };
}
updateProduct("123", { price: 29.99 }); // Only update price

// Required<T> — make all properties required
// Omit<T, K> — remove specific properties
// Pick<T, K> — keep only specific properties
type ProductPreview = Pick<Product, "name" | "price">;

// Record<K, V> — dictionary/object map type
const rolePermissions: Record<User["role"], string[]> = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
};

// Exclude<T, U> — remove from union type
type NonStringPrimitives = Exclude<string | number | boolean, string>; // number | boolean

// ReturnType<F> — get return type of function
type HandlerReturn = ReturnType<typeof handler>;

// Awaited<T> — unwrap Promise type
type UserData = Awaited<Promise<ApiResponse<User>>>; // ApiResponse<User>

// Custom utility type
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Makes ALL nested properties optional recursively
Enter fullscreen mode Exit fullscreen mode

Practical Patterns

// Pattern 1: Discriminated unions (type-safe state machines)
type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function renderUI(state: RequestState<User>) {
  switch (state.status) {
    case "idle":     return <p>Click to load</p>;
    case "loading":  return <Spinner />;
    case "success":  return <UserProfile data={state.data} />;
    case "error":    return <ErrorMessage error={state.error} />;
  }
  // TypeScript knows all cases are handled — no default needed!
}

// Pattern 2: Branded types (prevent mixing similar values)
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };

function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ */

const uid = createUserId("abc");
getUser(uid);      // OK
getOrder(uid);     // Error! UserId is not OrderId — even though both are strings!

// Pattern 3: Const assertions (literal types)
const CONFIG = {
  API_URL: "https://api.example.com",
  MAX_RETRIES: 3,
  TIMEOUT_MS: 5000,
} as const;
// CONFIG.API_URL is typed as literal "https://api.example.com", not string!

// Pattern 4: Template literal types
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;
type Endpoint = `${HttpMethod} ${ApiRoute}`; // e.g., "GET /api/users"

// Pattern 5: satisfies operator (TypeScript 4.9+)
const colors = {
  red: "#ff0000",
  green: "#00ff00",
  blue: "#0000ff",
} satisfies Record<string, string>; // Validates shape but keeps literal types
Enter fullscreen mode Exit fullscreen mode

Migration Strategy (JS → TS)

# Step 1: Install TypeScript
npm install -D typescript @types/node @types/express
npx tsc --init

# tsconfig.json essentials:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode
// Migration order:
// 1. Rename .js → .ts (immediate type coverage from inference!)
// 2. Add explicit types to function parameters and returns
// 3. Define interfaces for your data models
// 4. Enable strict mode options one by one
// 5. Add JSDoc @types for third-party libs without .d.ts files

// Quick wins — add types to existing JS files without full migration:
// @ts-check at top of .js file enables type checking!
// /** @type {number} */ for variable annotations
// /** @param {string} name */ for parameter annotations
Enter fullscreen mode Exit fullscreen mode

What's your favorite TypeScript feature? What confused you most when learning it?

Follow @armorbreak for more practical developer guides.

Top comments (0)