TypeScriptJavaScriptSoftware Architecture

Bulletproof TypeScript Patterns for Production Apps

Production-ready TypeScript patterns that will help you write more maintainable and robust applications.

5 min read
A
AnhDojo
Bulletproof TypeScript Patterns for Production Apps

Bulletproof TypeScript Patterns for Production Apps

TypeScript has become the standard choice for large-scale JavaScript applications, but using it effectively requires more than just adding type annotations. In this post, I’ll share battle-tested patterns I use in production applications to maximize type safety while maintaining code flexibility.

Discriminated Unions for State Management

When managing state, especially with reducers, discriminated unions provide compile-time guarantees about which actions can be performed in which states:

type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
// Usage
function UserProfile() {
const [userState, setUserState] = useState<RequestState<User>>({
status: "idle",
});
useEffect(() => {
const fetchUser = async () => {
setUserState({ status: "loading" });
try {
const user = await api.getUser();
setUserState({ status: "success", data: user });
} catch (error) {
setUserState({
status: "error",
error: error instanceof Error ? error : new Error(String(error)),
});
}
};
fetchUser();
}, []);
// Type-safe rendering based on state
if (userState.status === "loading") {
return <Spinner />;
}
if (userState.status === "error") {
return <ErrorMessage message={userState.error.message} />;
}
if (userState.status === "success") {
// TypeScript knows userState.data is available here
return <UserDetails user={userState.data} />;
}
return (
<button onClick={() => setUserState({ status: "loading" })}>
Load User
</button>
);
}

The Builder Pattern for Complex Objects

When creating objects with many optional properties, the builder pattern provides a fluent interface with type safety:

class QueryBuilder<T> {
private query: Partial<T> = {};
where<K extends keyof T>(key: K, value: T[K]): this {
this.query[key] = value;
return this;
}
whereIn<K extends keyof T>(key: K, values: Array<T[K]>): this {
// Implementation
return this;
}
orderBy<K extends keyof T>(key: K, direction: "asc" | "desc" = "asc"): this {
// Implementation
return this;
}
limit(limit: number): this {
// Implementation
return this;
}
build(): Partial<T> {
return { ...this.query };
}
}
// Usage
const usersQuery = new QueryBuilder<User>()
.where("status", "active")
.whereIn("role", ["admin", "editor"])
.orderBy("createdAt", "desc")
.limit(10)
.build();

Type Guards for Runtime Validation

Type guards bridge the gap between compile-time and runtime type checking:

interface User {
id: string;
name: string;
email: string;
}
// Type guard function
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"name" in obj &&
"email" in obj &&
typeof (obj as User).id === "string" &&
typeof (obj as User).name === "string" &&
typeof (obj as User).email === "string"
);
}
// Usage with API responses
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
if (isUser(data)) {
return data; // TypeScript knows data is User
}
throw new Error("Invalid user data received from API");
}

Branded Types for Type Safety

Sometimes you need to differentiate between types with the same structure but different semantics:

// Define branded types
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
// Create factory functions
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
// Now these functions will only accept the correct type
function fetchUser(id: UserId) {
// Implementation
}
function fetchOrder(id: OrderId) {
// Implementation
}
// Usage
const userId = createUserId("user-123");
const orderId = createOrderId("order-456");
fetchUser(userId); // ✓ OK
fetchOrder(orderId); // ✓ OK
fetchUser(orderId); // ✗ Type error: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'

Advanced Type Utilities

TypeScript’s type system is Turing-complete, allowing for powerful type transformations:

// Make all properties optional and nullable
type Nullable<T> = { [P in keyof T]: T[P] | null };
// Extract the type of a Promise's resolved value
type Awaited<T> = T extends Promise<infer R> ? R : T;
// Make specific properties required
type RequireKeys<T, K extends keyof T> = T & { [P in K]-?: T[P] };
// Create a type with only the specified keys
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Remove readonly modifier from properties
type Mutable<T> = { -readonly [P in keyof T]: T[P] };

Dependency Injection with Interfaces

For testable, maintainable code, use interfaces for services and inject dependencies:

interface Logger {
debug(message: string): void;
info(message: string): void;
warn(message: string): void;
error(message: string, error?: Error): void;
}
interface UserService {
getUser(id: string): Promise<User>;
updateUser(user: User): Promise<User>;
}
class UserController {
constructor(private userService: UserService, private logger: Logger) {}
async handleGetUser(id: string): Promise<User> {
this.logger.info(`Fetching user: ${id}`);
try {
const user = await this.userService.getUser(id);
return user;
} catch (error) {
this.logger.error("Failed to get user", error as Error);
throw error;
}
}
}
// Easy to test with mocks
const mockLogger: Logger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
const mockUserService: UserService = {
getUser: jest.fn(),
updateUser: jest.fn(),
};
const controller = new UserController(mockUserService, mockLogger);

Conclusion

TypeScript’s power goes far beyond simple type annotations. By leveraging these patterns, you can build applications that are not only type-safe but also more maintainable, testable, and robust in production.

What are your favorite TypeScript patterns? Share in the comments!

AD

Written by Anh Dojo

Backend developer passionate about building scalable systems and sharing knowledge with the community.