Learn how to write better, more type-safe React code with TypeScript
TypeScript has become the de facto standard for building modern React applications. In this post, we'll explore best practices for writing type-safe React code.
Always type your component props explicitly:
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export default function Button({
label,
onClick,
variant = 'primary',
disabled = false,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
Let TypeScript infer types when possible:
// Good - type is inferred
const [count, setCount] = useState(0);
// Unnecessary - type is already inferred
const [count, setCount] = useState<number>(0);
Use discriminated unions for complex state:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function useAsyncData<T>(fetchFn: () => Promise<T>) {
const [state, setState] = useState<AsyncState<T>>({
status: 'idle',
});
// TypeScript now knows the exact shape based on status
if (state.status === 'success') {
console.log(state.data); // ✅ data is available
}
if (state.status === 'error') {
console.log(state.error); // ✅ error is available
}
}
Create reusable generic components:
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
}
export default function Select<T>({
options,
value,
onChange,
getLabel,
}: SelectProps<T>) {
return (
<select
value={getLabel(value)}
onChange={(e) => {
const option = options.find(
(o) => getLabel(o) === e.target.value
);
if (option) onChange(option);
}}
>
{options.map((option) => (
<option key={getLabel(option)} value={getLabel(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}
Type your event handlers correctly:
interface FormProps {
onSubmit: (data: FormData) => void;
}
export default function Form({ onSubmit }: FormProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit(formData);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
}
Leverage TypeScript utility types:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// Pick specific fields
type UserPreview = Pick<User, 'id' | 'name'>;
// Make all fields optional
type PartialUser = Partial<User>;
// Make all fields required
type RequiredUser = Required<User>;
// Omit specific fields
type PublicUser = Omit<User, 'role'>;
any
Always prefer unknown
over any
:
// Bad
function processData(data: any) {
return data.value; // No type checking
}
// 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');
}
TypeScript significantly improves the development experience and code quality in React applications. By following these best practices, you'll write more maintainable and type-safe code.
Remember:
any
- use unknown
insteadHappy typing!