LOADING BASE0%
Portfolio Logo
Back to posts
TypeScriptArchitectureEngineering

TypeScript Advanced Patterns for Scalable Startups

3 min read

Stop Writing any

As an engineer, one of my biggest pet peeves when reviewing PRs from junior devs migrating from JavaScript is seeing any used as an "escape hatch" when the compiler gets angry.

If you are using any in a startup stack, you are opting out of the primary benefit of TypeScript: Type Safety. The moment a runtime error slips into production because user.profile.bio was actually undefined, your compiler has failed you.

1. Generics are Not Scary

If I had a dollar for every time I saw a duplicate interface just to change the data field payload type, I could single-handedly fund XRide Labs. Generics let you create reusable component templates.

// BAD: Duplication
interface UserResponse {
  statusCode: number;
  data: User;
}
interface ProductResponse {
  statusCode: number;
  data: Product;
}
 
// GOOD: Generic Pattern
interface ApiResponse<T> {
  statusCode: number;
  data: T;
  message?: string;
}
 
// Usage
const fetchUser = async (): Promise<ApiResponse<User>> => { ... }

2. Using Zod for the Boundary Layer

TypeScript only protects you at compile time. If your Node.js backend receives a JSON payload from a mobile client, TypeScript has no idea if the payload is actually structured the way you expect. Zod protects your runtime boundaries.

import { z } from "zod";
 
// 1. Define the Schema (Runtime protection)
export const UserRegistrationSchema = z.object({
  email: z.string().email(),
  username: z.string().min(3),
  age: z.number().min(18).optional(),
});
 
// 2. Infer the Type (Compile-time protection)
export type UserRegistration = z.infer<typeof UserRegistrationSchema>;
 
// Validate incoming requests!
app.post("/register", (req, res) => {
  const result = UserRegistrationSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: result.error.format() });
  }
 
  // result.data is now absolutely type-safe and validated
  const user: UserRegistration = result.data;
});

3. Discriminated Unions for React Props

Creating highly reusable UI components often leads to what I call "Prop Soup". A button component that takes href, onClick, isLoading, and target might accidentally end up accepting all of them at once.

// Using a Discriminated Union prevents invalid combinations!
type ButtonBaseProps = {
  variant: "primary" | "secondary";
  children: React.ReactNode;
};
 
type LinkButtonProps = ButtonBaseProps & {
  as: "link";
  href: string;
  target?: string;
};
 
type ActionButtonProps = ButtonBaseProps & {
  as: "button";
  onClick: () => void;
  isLoading?: boolean;
};
 
type ButtonProps = LinkButtonProps | ActionButtonProps;
 
export const Button = (props: ButtonProps) => {
  // TypeScript will force you to check 'as' before using specific props!
  if (props.as === "link") {
    return <a href={props.href}>{props.children}</a>;
  }
  return <button onClick={props.onClick}>{props.children}</button>;
};

Learn the type system deeply, and you'll find your runtime bugs practically vanish.