TypeScript and React make a powerful combination. Here are essential tips to level up your TypeScript skills in React projects:
Always type your component props:
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
onClick: () => void;
children: React.ReactNode;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
variant,
size = 'medium',
onClick,
children,
disabled = false
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
Create reusable components with generics:
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
Properly type event handlers:
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Handle form submission
};
Leverage TypeScript utility types:
// Extract props from existing components
type DivProps = React.ComponentProps<'div'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Pick specific properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Omit properties
type UserWithoutPassword = Omit<User, 'password'>;
type ApiResponse<T> = T extends string
? { message: T }
: { data: T };
// Usage
type StringResponse = ApiResponse<string>; // { message: string }
type UserResponse = ApiResponse<User>; // { data: User }
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Make email optional in User type
type UserWithOptionalEmail = Optional<User, 'email'>;
type EventName = `on${Capitalize<string>}`;
type ButtonEvent = `button${Capitalize<'click' | 'hover' | 'focus'>}`;
// Results in: 'buttonClick' | 'buttonHover' | 'buttonFocus'
interface FormData {
name: string;
email: string;
age: number;
}
const useForm = <T extends Record<string, any>>(
initialValues: T,
validate: (values: T) => Partial<Record<keyof T, string>>
) => {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const handleChange = (field: keyof T) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.type === 'number' ? +e.target.value : e.target.value;
setValues(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = (onSubmit: (values: T) => void) => (
e: React.FormEvent
) => {
e.preventDefault();
const newErrors = validate(values);
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
onSubmit(values);
}
};
return { values, errors, handleChange, handleSubmit };
};
These patterns will help you write more robust and maintainable React applications with TypeScript.