JavaScript25 min readAdvanced

TypeScript: The Practical Bits

Just enough TypeScript to ship faster and safer — types, unions, generics, and the parts everyone actually uses.

Why TypeScript

TypeScript is JavaScript with optional static types. You write code that looks like JS, add type annotations where you want, and the TypeScript compiler (tsc) checks types at compile time and emits plain JS. At runtime, types are GONE — they exist purely to help you and your IDE catch mistakes earlier.

Most large JavaScript codebases now use TypeScript. The reasons: autocomplete that actually works, refactors that don't break things in production, and self-documenting function signatures.

Basic types

let name: string = "Ada";
let age: number = 36;
let active: boolean = true;
let tags: string[] = ["js", "ts"];
let pair: [number, string] = [1, "one"];   // tuple
let anything: unknown = JSON.parse("{}");    // \"any\" but safer

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

You can almost always omit type annotations on variables — TypeScript infers them from the assigned value. Annotate function parameters and return types; let inference handle the rest.

Type aliases and interfaces

type User = {
  id: string;
  name: string;
  admin?: boolean;       // optional
};

interface Post {
  id: string;
  title: string;
  authorId: string;
}

function greet(u: User): string {
  return u.admin ? `Welcome back, boss ${u.name}` : `Hi ${u.name}`;
}

`type` and `interface` are mostly interchangeable for object shapes. Use `interface` when you want declaration merging or class-y inheritance; use `type` for everything else (unions, primitives, mapped types).

Union types — the killer feature

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

function render(s: Status) {
  if (s === "loading") return "...";
  if (s === "error") return "!";
  return ":)";
}

// Discriminated unions — the cleanest pattern in TS
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function useResult(r: Result<number>) {
  if (r.ok) {
    console.log(r.value);    // TS knows .value exists here
  } else {
    console.log(r.error);    // and .error exists here
  }
}

Generics

Generics let you write code that works with any type, while keeping type information. Think of `<T>` as a type parameter — a placeholder filled in at the call site.

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

const n = first([1, 2, 3]);          // n: number | undefined
const s = first(["a", "b"]);         // s: string | undefined

Working with arrays of objects

type Order = { id: string; total: number; status: "open" | "paid" };

function totalRevenue(orders: Order[]): number {
  return orders
    .filter(o => o.status === "paid")
    .reduce((sum, o) => sum + o.total, 0);
}

Things you can skip at first

  • Conditional types (`T extends U ? X : Y`).
  • Mapped types (`{ [K in keyof T]: ... }`).
  • Template literal types (`\`prefix-\${string}\``).
  • All the utility types beyond `Partial`, `Pick`, and `Omit`.
💡 Tip
Type the boundaries — function signatures, API responses, props. Let inference handle the inside. You don't need to annotate every variable; that's noise. The goal is safety, not bureaucracy.