Skip to main content
TypeScript Advanced Patterns: Generics, Utility Types, and Type Guards

TypeScript Advanced Patterns: Generics, Utility Types, and Type Guards

Web DevelopmentDecember 26, 202518 min read0 views
TypeScriptJavaScriptProgrammingTypesBest PracticesTutorial
Share:

TypeScript Advanced Patterns: Generics, Utility Types, and Type Guards

TypeScript has revolutionized JavaScript development by adding static typing. But to truly leverage its power, you need to master advanced patterns. Let's dive deep into generics, utility types, and type guards.

Why Advanced TypeScript Matters

Writing TypeScript is easy. Writing good TypeScript that's maintainable, type-safe, and elegant requires understanding advanced patterns. These patterns help you:

  • 🎯 Catch bugs at compile time
  • 📚 Improve code documentation
  • 🔄 Enable better refactoring
  • 💡 Enhance IDE autocomplete

Generics: Write Once, Use Everywhere

Generics allow you to write reusable code that works with multiple types.

Basic Generic Function

function identity<T>(arg: T): T { return arg; } const num = identity<number>(42); // type: number const str = identity<string>("hello"); // type: string const auto = identity(true); // type inferred: boolean

Generic Constraints

interface Lengthwise { length: number; } function logLength<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } logLength("hello"); // ✅ Works logLength([1, 2, 3]); // ✅ Works logLength({ length: 10 }); // ✅ Works // logLength(42); // ❌ Error: number doesn't have length

Generic Interfaces

interface Repository<T> { findById(id: string): Promise<T | null>; findAll(): Promise<T[]>; create(item: Omit<T, "id">): Promise<T>; update(id: string, item: Partial<T>): Promise<T>; delete(id: string): Promise<void>; } interface User { id: string; name: string; email: string; } class UserRepository implements Repository<User> { async findById(id: string): Promise<User | null> { // Implementation return null; } async findAll(): Promise<User[]> { return []; } async create(item: Omit<User, "id">): Promise<User> { const user: User = { id: "generated", ...item }; return user; } async update(id: string, item: Partial<User>): Promise<User> { // Implementation return { id, name: "", email: "" }; } async delete(id: string): Promise<void> { // Implementation } }

Utility Types: TypeScript's Swiss Army Knife

Partial<T> - Make All Properties Optional

interface User { id: string; name: string; email: string; age: number; } function updateUser(id: string, updates: Partial<User>) { // Can update any subset of properties } updateUser("1", { name: "John" }); // ✅ updateUser("2", { email: "new@email.com" }); // ✅ updateUser("3", { name: "Jane", age: 30 }); // ✅

Pick<T, K> - Select Specific Properties

type UserPreview = Pick<User, "id" | "name">; const preview: UserPreview = { id: "1", name: "John" // email and age not required };

Omit<T, K> - Exclude Specific Properties

type UserCreate = Omit<User, "id">; const newUser: UserCreate = { name: "John", email: "john@example.com", age: 30 // id is excluded };

Record<K, T> - Create Object Type with Specific Keys

type Role = "admin" | "user" | "guest"; type Permissions = Record<Role, string[]>; const permissions: Permissions = { admin: ["read", "write", "delete"], user: ["read", "write"], guest: ["read"] };

ReturnType<T> - Extract Function Return Type

function createUser() { return { id: "123", name: "John", email: "john@example.com" }; } type User = ReturnType<typeof createUser>; // type User = { id: string; name: string; email: string; }

Type Guards: Runtime Type Safety

typeof Type Guard

function processValue(value: string | number) { if (typeof value === "string") { // TypeScript knows value is string here return value.toUpperCase(); } else { // TypeScript knows value is number here return value.toFixed(2); } }

instanceof Type Guard

class Dog { bark() { console.log("Woof!"); } } class Cat { meow() { console.log("Meow!"); } } function makeSound(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark(); // TypeScript knows it's a Dog } else { animal.meow(); // TypeScript knows it's a Cat } }

Custom Type Guards

interface Fish { swim: () => void; } interface Bird { fly: () => void; } // User-defined type guard function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; } function move(pet: Fish | Bird) { if (isFish(pet)) { pet.swim(); // TypeScript knows pet is Fish } else { pet.fly(); // TypeScript knows pet is Bird } }

Discriminated Unions

interface Success { type: "success"; data: any; } interface Error { type: "error"; message: string; } type Result = Success | Error; function handleResult(result: Result) { switch (result.type) { case "success": console.log(result.data); // TypeScript knows it's Success break; case "error": console.log(result.message); // TypeScript knows it's Error break; } }

Advanced Pattern: Conditional Types

type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false // More practical example type NonNullable<T> = T extends null | undefined ? never : T; type A = NonNullable<string | null>; // string type B = NonNullable<number | undefined>; // number

Advanced Pattern: Mapped Types

type Readonly<T> = { readonly [P in keyof T]: T[P]; }; type Optional<T> = { [P in keyof T]?: T[P]; }; interface User { id: string; name: string; } type ReadonlyUser = Readonly<User>; // { readonly id: string; readonly name: string; } type OptionalUser = Optional<User>; // { id?: string; name?: string; }

Real-World Example: API Response Handler

interface ApiResponse<T> { data?: T; error?: string; status: number; } // Generic fetch wrapper async function fetchApi<T>(url: string): Promise<ApiResponse<T>> { try { const response = await fetch(url); const data = await response.json(); return { data, status: response.status }; } catch (error) { return { error: error instanceof Error ? error.message : "Unknown error", status: 500 }; } } // Type-safe usage interface User { id: string; name: string; } async function getUser(id: string) { const response = await fetchApi<User>(`/api/users/${id}`); if (response.error) { console.error(response.error); return null; } return response.data; // TypeScript knows this is User | undefined }

Best Practices

  1. Use Generics for Reusability: Write once, use with many types
  2. Leverage Utility Types: Don't reinvent the wheel
  3. Create Custom Type Guards: Improve runtime type safety
  4. Use Discriminated Unions: For complex state management
  5. Keep Types Simple: Don't over-engineer

Common Pitfalls to Avoid

Too Generic

function process<T>(data: T): T { // Too generic, loses type information return data; }

Properly Constrained

function process<T extends { id: string }>(data: T): T { console.log(data.id); // Type-safe access return data; }

Conclusion

Advanced TypeScript patterns transform your code from "typed JavaScript" to truly type-safe, maintainable applications. Master these patterns, and you'll write better code with fewer bugs.

Start small, practice often, and gradually incorporate these patterns into your projects. Your future self (and teammates) will thank you! 🚀

Let's Connect

Ready to build something amazing together?

Send us a message

🚀

Let's Chat.

Tell me about your project.

Let's create something together 🤝

Visit my social profile and get connected