December 10, 2023
TypeScript Best Practices for React Developers
A comprehensive guide to writing better TypeScript code in React applications, with practical examples and patterns.
TypeScript · React · Best Practices
Introduction
TypeScript has become the de facto standard for building robust React applications. In this guide, I'll share essential TypeScript best practices specifically tailored for React developers.
1. Properly Type Your Components
Always define explicit types for your component props:
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = 'primary',
disabled = false,
}) => {
return (
<button onClick={onClick} disabled={disabled} className={variant}>
{label}
</button>
);
};2. Use Discriminated Unions
For state that can be in different modes, use discriminated unions:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function DataComponent() {
const [state, setState] = useState<AsyncState<User>>({
status: 'idle',
});
if (state.status === 'loading') return <Spinner />;
if (state.status === 'error') return <Error error={state.error} />;
if (state.status === 'success') return <UserProfile user={state.data} />;
return null;
}3. Leverage Type Inference
Don't over-specify types when TypeScript can infer them:
// ❌ Unnecessary
const [count, setCount] = useState<number>(0);
// ✅ Better - TypeScript infers the type
const [count, setCount] = useState(0);
// ✅ Good - When initial value doesn't match the full type
const [user, setUser] = useState<User | null>(null);4. Use Generics for Reusable Components
Create flexible, type-safe components with generics:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>;5. Strict Null Checks
Always enable strict null checks in `tsconfig.json`:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true
}
}Handle nullable values explicitly:
// ❌ Unsafe
function greet(name: string | null) {
return `Hello, ${name.toUpperCase()}`; // Error!
}
// ✅ Safe
function greet(name: string | null) {
return `Hello, ${name?.toUpperCase() ?? 'Guest'}`;
}6. Use Const Assertions
Make your objects and arrays more specific:
// Without const assertion
const colors = ['red', 'blue', 'green']; // string[]
// With const assertion
const colors = ['red', 'blue', 'green'] as const;
// readonly ['red', 'blue', 'green']
type Color = (typeof colors)[number]; // 'red' | 'blue' | 'green'7. Avoid `any` - Use `unknown` Instead
When you truly don't know the type, use `unknown`:
// ❌ Bad
function processData(data: any) {
return data.value; // No type safety
}
// ✅ Good
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: string }).value;
}
throw new Error('Invalid data');
}8. Create Utility Types
Build reusable type utilities:
// Make all properties optional recursively
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Extract component props type
type ButtonProps = React.ComponentProps<typeof Button>;
// Pick specific props
type UserNameProps = Pick<User, 'firstName' | 'lastName'>;9. Type Your Hooks Properly
Custom hooks should have proper return types:
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse } as const;
}
// Usage with proper types
const { value, toggle } = useToggle();10. Use Type Guards
Create type guards for runtime checks:
interface User {
type: 'user';
name: string;
}
interface Admin {
type: 'admin';
name: string;
permissions: string[];
}
type Person = User | Admin;
function isAdmin(person: Person): person is Admin {
return person.type === 'admin';
}
function greet(person: Person) {
if (isAdmin(person)) {
// TypeScript knows person is Admin here
console.log(person.permissions);
}
}Conclusion
These TypeScript best practices will help you write more maintainable, type-safe React applications. Remember, TypeScript is there to help you catch errors early and provide better IDE support—embrace it!
Start implementing these patterns in your projects today, and you'll see immediate improvements in code quality and developer experience.
Happy coding! 💙