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.