Using TypeScript with React Components and Hooks:
- Always define the prop types for your components. Use TypeScript’s
interface
ortype
to describe the shape of props. For example:interface ButtonProps { label: string; onClick: () => void; disabled?: boolean }
. Thenconst Button: React.FC<ButtonProps> = ...
. Similarly, if you useuseState
with 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 | number
rather 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 }
. TheT
will be the type of data the hook fetches, allowing the consumer to specify it (e.g.,useFetch<User[]>('/api/users')
would makedata
aUser[]
). - 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
ComponentNameProps
is 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
createContext
as 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,
onChange
for an input can be typed asReact.ChangeEvent<HTMLInputElement>
, and anonClick
asReact.MouseEvent<HTMLButtonElement>
. This provides autocompletion and type checking for event properties (likeevent.target.value
will 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 aUser
interface 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/users
that 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
zod
orio-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
unknown
overany
for JSON: It’s better to cast tounknown
and 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 unknown
then 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.json
enable strict mode and related flags:"strict": true
which 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-react
andeslint-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 --fix
andtsc --noEmit
(type check) on changed files.