Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save matterai/c6d403bf2226db31a68948e26255a172 to your computer and use it in GitHub Desktop.
Save matterai/c6d403bf2226db31a68948e26255a172 to your computer and use it in GitHub Desktop.
ReactJS TypeScript Best Practices Prompt

Using TypeScript with React Components and Hooks:

  • Always define the prop types for your components. Use TypeScript’s interface or type to describe the shape of props. For example: interface ButtonProps { label: string; onClick: () => void; disabled?: boolean }. Then const Button: React.FC<ButtonProps> = .... Similarly, if you use useState with an initial null or empty value, specify the type to avoid it being any.
  • 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 as string | number rather than any. 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 (like useState<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 }. The T will be the type of data the hook fetches, allowing the consumer to specify it (e.g., useFetch<User[]>('/api/users') would make data a User[]).
  • 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 then const AuthContext = createContext<AuthContextValue | undefined>(undefined);. When using useReducer, 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 if useContext(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 as React.ChangeEvent<HTMLInputElement>, and an onClick as React.MouseEvent<HTMLButtonElement>. This provides autocompletion and type checking for event properties (like event.target.value will be correctly typed).
  • Type reducers’ actions. For example: type CounterAction = { type: 'increment' } | { type: 'decrement' }. Then reducer is const 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, and Omit<T, Keys> to remove some properties. For instance, if you have a User interface and you want a form that edits user but maybe not all fields, you could use Partial<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 use T 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 interface User { 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 or io-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 over any for JSON: It’s better to cast to unknown 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 with User[] data;

Enforcing Strict Type Safety (ESLint and tsconfig):

  • Enable Strict Flags: in tsconfig.json enable strict mode and related flags: "strict": true which encompasses noImplicitAny, 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 banning any (or requiring a comment justification for any) can maintain discipline.
  • Add eslint-plugin-react and eslint-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 and tsc --noEmit (type check) on changed files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment