Mastering TypeScript: Advanced Patterns and Best Practices

Mastering TypeScript: Advanced Patterns and Best Practices

6 min read
by Jane Smith

Dive deep into advanced TypeScript patterns, type system features, and best practices for building scalable applications.

typescriptjavascriptprogrammingbest practices

Introduction to Advanced TypeScript

TypeScript has become an essential tool for modern JavaScript development. While many developers are comfortable with basic types, mastering advanced TypeScript patterns can significantly improve your code quality and developer experience.

Advanced Type System Features

Generic Constraints

One of the most powerful features in TypeScript is the ability to constrain generic types:

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("Hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ array has length
logLength({ length: 10, value: 3 }); // ✅ object has length

Conditional Types

Conditional types allow you to create types that depend on a condition:

type ApiResponse<T> = T extends string
  ? { message: T }
  : T extends number
  ? { code: T }
  : { data: T };

type StringResponse = ApiResponse<string>; // { message: string }
type NumberResponse = ApiResponse<number>; // { code: number }
type ObjectResponse = ApiResponse<User>; // { data: User }

Mapped Types

Create new types by transforming properties of existing types:

type Optional<T> = {
  [P in keyof T]?: T[P];
};

type ReadOnly<T> = {
  readonly [P in keyof T]: T[P];
};

interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Optional<User>; // All properties optional
type ImmutableUser = ReadOnly<User>; // All properties readonly

Utility Types in Action

TypeScript provides several built-in utility types that can save you time:

Pick and Omit

interface BlogPost {
  id: number;
  title: string;
  content: string;
  author: string;
  publishedAt: Date;
  tags: string[];
}

// Only pick specific properties
type BlogPreview = Pick<BlogPost, "id" | "title" | "author">;

// Omit specific properties
type CreateBlogPost = Omit<BlogPost, "id" | "publishedAt">;

Record Type

The Record type is perfect for creating index signatures:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

type ApiEndpoints = Record<HttpMethod, string[]>;

const endpoints: ApiEndpoints = {
  GET: ["/users", "/posts"],
  POST: ["/users", "/posts"],
  PUT: ["/users/:id", "/posts/:id"],
  DELETE: ["/users/:id", "/posts/:id"],
};

Design Patterns with TypeScript

Factory Pattern

interface Shape {
  area(): number;
}

class Circle implements Shape {
  constructor(private radius: number) {}

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  area(): number {
    return this.width * this.height;
  }
}

type ShapeType = "circle" | "rectangle";

class ShapeFactory {
  static createShape(type: ShapeType, ...args: number[]): Shape {
    switch (type) {
      case "circle":
        return new Circle(args[0]);
      case "rectangle":
        return new Rectangle(args[0], args[1]);
      default:
        throw new Error(`Unknown shape type: ${type}`);
    }
  }
}

Builder Pattern

class QueryBuilder {
  private query: string = "";

  select(fields: string[]): this {
    this.query += `SELECT ${fields.join(", ")} `;
    return this;
  }

  from(table: string): this {
    this.query += `FROM ${table} `;
    return this;
  }

  where(condition: string): this {
    this.query += `WHERE ${condition} `;
    return this;
  }

  build(): string {
    return this.query.trim();
  }
}

const query = new QueryBuilder()
  .select(["name", "email"])
  .from("users")
  .where("active = 1")
  .build();

Error Handling Best Practices

Result Type Pattern

type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

async function fetchUser(id: number): Promise<Result<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return {
        success: false,
        error: new Error(`HTTP ${response.status}`),
      };
    }

    const user = await response.json();
    return { success: true, data: user };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error("Unknown error"),
    };
  }
}

// Usage
const result = await fetchUser(123);
if (result.success) {
  console.log(result.data.name); // TypeScript knows this is User
} else {
  console.error(result.error.message);
}

Performance Considerations

Lazy Types

For large applications, consider using lazy type imports:

// Instead of importing the entire module
import { HeavyLibraryType } from "heavy-library";

// Use dynamic imports for types
type LazyType = import("heavy-library").HeavyLibraryType;

function processData(data: LazyType) {
  // Implementation
}

Template Literal Types

Create more specific string types:

type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue<T extends string> = `${T}${CSSUnit}`;

type Width = CSSValue<string>; // "10px" | "2em" | "100%" etc.

function setWidth(element: HTMLElement, width: Width) {
  element.style.width = width;
}

setWidth(element, "100px"); // ✅
setWidth(element, "invalid"); // ❌ Type error

Testing with TypeScript

Type Testing

Use conditional types to test your type definitions:

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
  ? 1
  : 2
  ? true
  : false;

// Test your types
type Test1 = Expect<Equal<Pick<User, "name">, { name: string }>>;
type Test2 = Expect<Equal<Omit<User, "id">, { name: string; email: string }>>;

Migration Strategies

When migrating JavaScript to TypeScript:

  1. Start with any and gradually add specific types
  2. Use @ts-ignore sparingly for quick fixes
  3. Enable strict mode incrementally
  4. Add types to new code first

Gradual Typing Example

// Step 1: Add basic types
function calculateTotal(items: any[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// Step 2: Add more specific types
interface Item {
  price: number;
  name: string;
}

function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// Step 3: Add validation
function calculateTotal(items: Item[]): number {
  if (!Array.isArray(items)) {
    throw new Error("Items must be an array");
  }

  return items.reduce((sum, item) => {
    if (typeof item.price !== "number") {
      throw new Error("Item price must be a number");
    }
    return sum + item.price;
  }, 0);
}

Conclusion

Mastering advanced TypeScript patterns takes time and practice, but the benefits are enormous:

  • Better code quality through type safety
  • Improved developer experience with autocomplete and error detection
  • Easier refactoring with confidence
  • Self-documenting code through type definitions

Start incorporating these patterns gradually into your projects, and you'll see immediate improvements in your development workflow.

Next Steps

To continue improving your TypeScript skills:

  1. Practice with the TypeScript playground
  2. Read the TypeScript handbook
  3. Contribute to open-source TypeScript projects
  4. Join the TypeScript community

Happy typing! 🚀