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.

CodeNotes
CodeNotes
CodeNotes
Why You Need TypeScript for React: 12 Practical Best Practices

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.FC

implicitly 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 array

Rule: 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptReActstrict modePropsuseStateuseRefgeneric components
CodeNotes
Written by

CodeNotes

Discuss code and AI, and document daily life and personal growth.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.