Deep Dive into TypeScript

TypeScript's type system goes far beyond simple annotations. Once you move past the basics, a handful of features unlock a completely different way of thinking about your code.

Generics

Generics let you write logic once and reuse it across types without losing safety. A common pitfall is reaching for any when a function needs to be flexible — a generic parameter is almost always the right tool instead.

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

The inferred return type follows the input automatically. No casting, no any.

Utility Types

TypeScript ships with a set of built-in mapped types that cover the most common transformations. Partial<T> makes every property optional, Required<T> does the opposite, and Pick<T, K> narrows a type to just the keys you need.

type UserPreview = Pick<User, "id" | "name" | "avatarUrl">;

Reaching for these before defining a new interface keeps your type graph from growing out of control.

Discriminated Unions

When a value can be one of several shapes, a discriminated union beats optional properties every time. Add a shared literal field — conventionally called kind or type — and TypeScript narrows automatically inside each branch.

type Result<T> =
  | { kind: "ok"; value: T }
  | { kind: "error"; message: string };

The exhaustiveness check is free: if you forget a case, the compiler tells you immediately.

Key Takeaway

These three features — generics, utility types, and discriminated unions — cover the majority of real-world TypeScript complexity. Master them and you will spend far less time fighting the type system and far more time using it as a design tool.