Using TypeScript with React Components and Hooks:
- Always define the prop types for your components. Use TypeScript’s
interfaceortypeto describe the shape of props. For example:interface ButtonProps { label: string; onClick: () => void; disabled?: boolean }. Thenconst Button: React.FC<ButtonProps> = .... Similarly, if you useuseStatewith an initial null or empty value, specify the type to avoid it beingany. - Avoid
any: use unknown or generics to handle flexible data, or union types if something can be multiple shapes. For example, if a prop can be either string or number, type it asstring | numberrather thanany. Enable strict flags in your tsconfig (strict: true) to help catch implicit any and other pitfalls. - Leverage Type Inference: You don’t need to annotate everything. For instance,
const [count, setCount] = useState(0)is inferred as number. But in cases where it can’t infer (likeuseState<YourType|null>(null)), provide a generic type. Strike a balance between explicitness and inference for clean code - Writing custom hooks, type their inputs and return values. You can use Generics for hooks that handle various types. For example, a
useFetch<T>hook might accept a URL and return{ data: T | null, error: Error | null, loading: boolean }. TheTwill be the type of data the hook fetches, allowing the consumer to specify it (e.g.,useFetch<User[]>('/api/users')would makedataaUser[]). - using
React.createContext, provide a type for the context value. You might have to initialize with a dummy value or use| undefined. One common pattern is to define a context value type, e.g.,interface AuthContextValue { user: User; login: () => void; logout: () => void; }and thenconst AuthContext = createContext<AuthContextValue | undefined>(undefined);. When usinguseReducer, define the action types as a TypeScript union for strict type checking on the reducer’s switch/case.
Defining Types for Props, State, and Context:
- Use either interface or type alias for defining props and state. For component props, an interface named
ComponentNamePropsis a common convention. - For complex component states that can be in different forms, consider using discriminated unions. For example, a request status state might be:
{ status: 'idle' } | { status: 'loading' } | { status: 'error', error: string } | { status: 'success', data: T }. - Define types for context values and use them in
createContextas above. You might want to throw ifuseContext(SomeContext)returns undefined (meaning the component is not wrapped in a provider), to catch usage errors early. - Leverage the types for event objects from React. For instance,
onChangefor an input can be typed asReact.ChangeEvent<HTMLInputElement>, and anonClickasReact.MouseEvent<HTMLButtonElement>. This provides autocompletion and type checking for event properties (likeevent.target.valuewill be correctly typed). - Type reducers’ actions. For example:
type CounterAction = { type: 'increment' } | { type: 'decrement' }. Then reducer isconst reducer = (state: number, action: CounterAction): number => { ... }. This ensures you handle all possible action types and don’t accidentally use an invalid action.
Utility Types and Generics for Reusable Components:
- Use
Partial<T>to make a type with all properties optional (handy for functions that take a config object),Required<T>to make all properties required,Pick<T, Keys>to select a few properties, andOmit<T, Keys>to remove some properties. For instance, if you have aUserinterface and you want a form that edits user but maybe not all fields, you could usePartial<User>for the form state type to start with all optional; - Use generics to make components flexible. A common example is a list component that can render a list of any type:
function List<T>({ items, renderItem }: { items: T[]; renderItem: (item: T) => React.ReactNode }) { ... }. - Enforcing Constraints with Generics: put constraints on generics using extends. For example,
function mergeObjects<T extends object, U extends object>(a: T, b: U): T & U { ... }; you might useT extends {} ? ...in more advanced scenarios like conditional prop types;
Handling API Responses with TypeScript Interfaces:
- Define API Data Types: Create TypeScript interfaces or types for the data structures you receive from APIs. If you have an endpoint
/api/usersthat returns{ id, name, email }, define an interfaceUser { id: number; name: string; email: string; }. Use these types in your code wherever you handle that data – e.g., the state that holds the data, or the prop passed into child components; - For critical data or external inputs, consider runtime validation (using libraries like
zodorio-ts) to verify the data matches the expected interface. - Always handle the undefined case in your code.
- When APIs return partial data or optional fields mark those fields as optional in your types (with
fieldName?: type). - Use of
unknownoveranyfor JSON: It’s better to cast tounknownand then type-narrow or assert it to the desired type after validation. For example:const result: unknown = await fetch(...).then(res => res.json()); // result is unknownthen use a type guard or validation to ensure it’s the type you expect. - Axios Response Types: using Axios or similar libraries, take advantage of generics they offer to type the response. For instance,
axios.get<User[]>('/api/users')will let the promise resolve withUser[]data;
Enforcing Strict Type Safety (ESLint and tsconfig):
- Enable Strict Flags: in
tsconfig.jsonenable strict mode and related flags:"strict": truewhich encompassesnoImplicitAny,strictNullChecks, etc. - Use ESLint with the TypeScript parser (
@typescript-eslint/parser) and include recommended rules. For example, rules like no-unused-vars (with TypeScript awareness) or banningany(or requiring a comment justification for any) can maintain discipline. - Add
eslint-plugin-reactandeslint-plugin-react-hooks. These will enforce the Rules of Hooks. - Use Prettier for code formatting;
- Integrate type checking and linting in your CI pipeline so that a build fails if types are broken. Also consider git pre-commit hooks (using something like Husky) to run
eslint --fixandtsc --noEmit(type check) on changed files.