Why You Need TypeScript for React: 12 Practical Best Practices
This article walks through twelve concrete TypeScript best‑practice patterns for React—covering props typing, avoiding React.FC, proper children and event types, useState and useRef inference, generic components, API‑driven type generation, strict mode, and utility types—to boost productivity and type safety.
Writing React without TypeScript often leads to painful refactoring and guessing prop types. TypeScript acts as a productivity amplifier, but only when used correctly. Below are twelve real‑world practices.
1. Props: type vs interface
// type
type ButtonProps = {
variant?: 'primary' | 'ghost';
onClick?: () => void;
};
// interface
interface ButtonProps {
variant?: 'primary' | 'ghost';
onClick?: () => void;
}Both work; choose one by team convention. Use type for union, intersection, Pick / Omit; use interface for extensible objects such as global configs.
2. Avoid React.FC
// ❌ not recommended
const Button: React.FC<ButtonProps> = ({ children, ...rest }) => { ... };
// ✅ modern style
function Button({ children, ...rest }: ButtonProps) {
return <button {...rest}>{children}</button>;
} React.FCimplicitly adds children, lacks generic support, and hurts defaultProps inference. The React team no longer recommends it.
3. Children type
type CardProps = {
children: React.ReactNode; // accepts any JSX children
};Do not use JSX.Element (single element only) or string (too narrow). If children must be a render function, type it as (data: User) => React.ReactNode.
4. Precise event types
// ❌ any
function Input({ onChange }: { onChange: (e: any) => void }) {}
// ✅ specific
function Input({ onChange }: { onChange: React.ChangeEvent<HTMLInputElement> => void }) {}Common event typings: React.ChangeEvent<HTMLInputElement> – input change React.FormEvent<HTMLFormElement> – form submit React.MouseEvent<HTMLButtonElement> – mouse click React.KeyboardEvent<HTMLInputElement> – keyboard React.FocusEvent<HTMLInputElement> – blur
5. useState generic inference
const [count, setCount] = useState(0); // inferred as number
const [user, setUser] = useState<User | null>(null); // explicit
const [list, setList] = useState<User[]>([]); // explicit for empty arrayRule: if the initial value’s type is clear, omit the generic; if it is null, undefined, or an empty array, declare the generic explicitly.
6. Three useRef patterns
// 1. DOM reference
const inputRef = useRef<HTMLInputElement>(null);
// 2. Mutable value without initial
const timerRef = useRef<number | null>(null);
// 3. Mutable value with initial
const countRef = useRef(0);When initialized with <HTMLInputElement>(null), TypeScript infers RefObject<HTMLInputElement> (read‑only current) suitable for the ref prop. Without an initial value, it infers MutableRefObject, which is writable but cannot be passed to a JSX ref.
7. Custom hook return types
function useUser() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
return { user, setUser, loading };
}TypeScript usually infers the return type, so explicit annotation is unnecessary unless returning a tuple:
function useToggle(): [boolean, () => void] {
const [value, setValue] = useState(false);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle]; // explicit to avoid union array type
}Alternatively, return as const to preserve tuple literal.
8. Extending native HTML attributes
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant, children, ...rest }: ButtonProps) {
return <button className={variant} {...rest}>{children}</button>;
}The spread ...rest automatically includes native props like onClick, disabled, and type, giving both flexibility and type safety.
9. Generic components
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>{items.map(it => <li key={keyExtractor(it)}>{renderItem(it)}</li>)}</ul>
);
}
// Usage – T inferred as User
<List items={users} renderItem={u => u.name} keyExtractor={u => u.id} />Although the syntax is a bit more complex, the component becomes fully reusable for any data shape.
10. Where to get type definitions
OpenAPI / Swagger – generate with openapi-typescript GraphQL – generate with graphql-codegen tRPC – end‑to‑end type safety between client and server
Zod schema – define once and infer TypeScript types via
z.infer<typeof schema> import { z } from 'zod';
const userSchema = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof userSchema>;Avoid hand‑writing API contracts; they diverge from the backend within months.
11. Enable strict mode
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array access yields T | undefined
"noImplicitOverride": true
}
}It may feel noisy at first, but the long‑term benefits are substantial. Start new projects with strict on; enable it gradually in existing codebases.
12. Common utility types
type UserName = Pick<User, 'name'>;
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type ReadonlyUser = Readonly<User>;
type UserMap = Record<string, User>;
type WithoutId = Omit<User, 'id'>;Adding ReturnType<typeof fn>, Parameters<typeof fn>, Awaited<Promise<T>>, etc., distinguishes proficient TypeScript users from those who are not.
Conclusion
React + TypeScript is a "premium combo" for modern front‑end development. By following the practices above—explicit prop typing, avoiding React.FC, proper children and event typings, disciplined useState / useRef usage, extending native attributes, leveraging generic components, generating types from APIs, enabling strict mode, and mastering utility types—developers gain safety without sacrificing productivity.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
