TypeScript has become essential for building large-scale React applications. In this post, I'll share some advanced TypeScript tips that have significantly improved my development workflow.
1. Proper Component Typing
Instead of using React.FC
, I prefer explicit prop typing:
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
}
export function Button({ label, onClick, variant = "primary" }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
This approach gives you more control and better type inference.
2. Utility Types Are Your Friends
TypeScript's built-in utility types are powerful:
// Pick specific properties
type UserBasicInfo = Pick<User, "name" | "email">;
// Omit properties
type UserWithoutPassword = Omit<User, "password">;
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
3. Discriminated Unions for State
Handle complex state with discriminated unions:
type LoadingState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: Data }
| { status: "error"; error: Error };
function handleState(state: LoadingState) {
switch (state.status) {
case "idle":
return <div>Ready to load</div>;
case "loading":
return <div>Loading...</div>;
case "success":
return <div>{state.data}</div>; // TypeScript knows data exists
case "error":
return <div>{state.error.message}</div>; // TypeScript knows error exists
}
}
4. Generic Components
Create reusable components with generics:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
// Usage with full type safety
<List items={users} renderItem={(user) => <li>{user.name}</li>} />;
5. Type Guards
Implement type guards for better type narrowing:
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"email" in value
);
}
// Now TypeScript knows the type
if (isUser(data)) {
console.log(data.name); // Type-safe!
}
6. As Const for Better Inference
Use as const
for literal types:
const COLORS = ["red", "blue", "green"] as const;
type Color = (typeof COLORS)[number]; // 'red' | 'blue' | 'green'
7. Typed Event Handlers
Properly type event handlers:
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
console.log(event.target.value); // Fully typed
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
// Handle form submission
}
8. Extract Types from Components
Extract prop types when needed:
import { ComponentProps } from "react";
type ButtonProps = ComponentProps<"button">;
type DivProps = ComponentProps<"div">;
Best Practices
- Enable Strict Mode: Always use
"strict": true
in tsconfig.json - Avoid Any: Use
unknown
instead ofany
when type is uncertain - Use Type Inference: Let TypeScript infer types when possible
- Document Complex Types: Add JSDoc comments for complex type definitions
- Keep Types Close: Define types near where they're used for better maintainability
Conclusion
TypeScript significantly improves the development experience and code quality in React applications. These patterns have saved me countless hours of debugging and made my code more maintainable.
What are your favorite TypeScript patterns? Let me know on social media!