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.