3 Topics
Chapter•75 Minutes

Application Design and TypeScript Best Practices

Apply types to API contracts, domain models, reusable libraries, error boundaries, and maintainable application architecture.

Domain Types and API Contracts

Separate domain models from transport DTOs when their responsibilities differ. A database entity may contain internal fields, while a public API response should expose only approved data. Use explicit conversion functions at boundaries so transformations are visible and testable.

Avoid assuming that a type alias used by both server and client automatically guarantees wire compatibility. Types can drift, and JSON cannot represent every JavaScript value (such as Date objects) in the same way. Document serialized formats and validate incoming data.

Map an internal model to a response
type UserEntity = {
  id: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
};

type UserResponse = {
  id: string;
  email: string;
  createdAt: string;
};

function toUserResponse(user: UserEntity): UserResponse {
  return {
    id: user.id,
    email: user.email,
    createdAt: user.createdAt.toISOString()
  };
}
💡 The conversion function prevents the password hash from leaking and serializes the date as a string. Explicit mapping makes the API contract reviewable.
Key Takeaways
  • Separate internal entities from public DTOs when fields differ.
  • Map and serialize values deliberately.
  • Never expose sensitive internal fields just because they exist on a shared type.
Knowledge Check

Why map a database entity to a separate API response type?

Composition and Dependency Boundaries

Types make dependencies explicit. A service can depend on an interface-shaped capability instead of a concrete database or network implementation, making it easier to test and swap infrastructure. TypeScript interfaces are erased at runtime, however, so dependency injection needs runtime tokens, factory functions, or concrete values depending on the framework.

Use small interfaces that describe what a consumer needs rather than mirroring an entire library. This reduces coupling and makes unit tests simpler. Do not create abstractions solely to satisfy a type-system pattern; the boundary should represent a meaningful responsibility.

Depend on a capability
interface UserRepository {
  findById(id: string): Promise<{ id: string; name: string } | null>;
}

class UserService {
  constructor(private readonly users: UserRepository) {}

  async displayName(id: string): Promise<string> {
    const user = await this.users.findById(id);
    return user ? user.name : "Unknown user";
  }
}
💡 UserService depends on the repository contract rather than a specific database. Tests can provide an in-memory implementation with the same method signature.
Key Takeaways
  • Depend on the smallest useful capability.
  • TypeScript interfaces do not exist as runtime injection tokens.
  • Abstractions are valuable when they reduce real coupling.
Knowledge Check

What is a primary benefit of typing a service against a repository interface?

Avoiding any, Understanding never, and Assertions

any opts out of checking and can spread unsafety through a codebase. unknown is a safer top type. never represents an impossible value, often appearing when a function always throws or when a union has been exhaustively narrowed. Type assertions such as value as User tell the compiler to trust you; they do not convert or validate the value.

Use assertions only when you have evidence the compiler cannot express, such as knowledge from a validated boundary or a DOM lookup with a known element type. Keep assertions local, and avoid double assertions such as as unknown as X except in carefully justified interop code.

Use never for exhaustive checks
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2;
    case "square": return shape.side ** 2;
    default: {
      const impossible: never = shape;
      return impossible;
    }
  }
}
💡 After both variants are handled, shape should be never. If another shape is added and not handled, the assignment reports an error.
Key Takeaways
  • any disables checks; unknown requires narrowing.
  • never is useful for impossible states and exhaustiveness.
  • Assertions change the compiler's view, not the runtime value.
Knowledge Check

Does `value as User` validate the value at runtime?