Skip to main content

best-practices-ts-react

Best Practices for Using TypeScript with React:

Strongly Type Props and State:

You should always give types to your props and state. This makes your code safer and can help catch mistakes early. For example, if you have a component that takes a title and an age, you should define types for both.

  • Code Example:

    type MyComponentProps = {
    title: string;
    age: number;
    };

    type MyComponentState = {
    count: number;
    };

    class MyComponent extends React.Component<MyComponentProps, MyComponentState> {
    // ...
    }

Functional Components with Hooks:

Functional components are simpler and should be used whenever possible. Hooks like useState and useEffect are part of this. You can let TypeScript figure out the types (type inference) or you can set them yourself for more complex situations.

  • Code Example:

    type MyComponentProps = {
    title: string;
    };

    const MyComponent: React.FC<MyComponentProps> = ({ title }) => {
    const [count, setCount] = React.useState(0); // TypeScript infers 'count' is a number

    // When state is complex, you can define the type
    type ComplexState = { clicks: number; isActive: boolean };
    const [complexState, setComplexState] = React.useState<ComplexState>({
    clicks: 0,
    isActive: false,
    });

    // ...
    };

Use TypeScript’s Utility Types:

TypeScript has many utility types that can help you work with types in a smart way. Partial makes all properties optional, Readonly makes them all readonly, and Record creates a type with a set of properties.

  • Code Example:

    type User = {
    id: number;
    name: string;
    };

    // Partial makes all properties of User optional
    function updateUser(id: number, changes: Partial<User>) {
    // ...
    }

    // Readonly prevents the modification of properties
    const user: Readonly<User> = { id: 1, name: 'John' };

    // Record creates a type with properties of the same type
    const usersById: Record<number, User> = {
    1: { id: 1, name: 'John' },
    2: { id: 2, name: 'Jane' },
    };

Type Custom Hooks:

When you make your own hooks, you should define the type of the value it returns. This helps make sure that your hook is used correctly wherever you use it.

  • Code Example:
    function useCustomHook(): [number, React.Dispatch<React.SetStateAction<number>>] {
    const [value, setValue] = React.useState(0);
    // Other logic
    return [value, setValue];
    }

Type Event Handlers:

You should use specific event handler types, like React.MouseEventHandler, to make sure you're using the right type of event. This helps catch mistakes where the wrong event type is used.

  • Code Example:

    const MyButton: React.FC = () => {
    const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
    // ...
    };

    return <button onClick={handleClick}>Click me</button>;
    };

Avoid any for Props:

Do not use any for the types of props. Even if it's tempting to use any when the type is complicated, it's better to define the complex type. This can help find mistakes early.

  • Code Example:

    type ComplexProps = {
    data: {
    id: number;
    name: string;
    // More complex properties
    };
    // Other props
    };

    const MyComponent: React.FC<ComplexProps> = ({ data }) => {
    // Correctly typed, no use of 'any'
    // ...
    };

Type Assertions with Caution:

Type assertions should be used sparingly. They tell TypeScript you know the type better than it does, which can be risky. Only use them when you're certain of the underlying data type and TypeScript can't figure it out on its own.

  • Code Example:
    const myElement = document.getElementById('myElement') as HTMLDivElement;
    // Use this when you are certain 'myElement' is a div.

Higher-Order Components (HOCs) and TypeScript:

With HOCs, which are functions that take a component and return a new component, it's important to keep the prop types correct. Make sure you're passing the generic type parameters along so the types stay consistent.

  • Code Example:
    function withExtraProps<T>(WrappedComponent: React.ComponentType<T>) {
    return class extends React.Component<T & { extraProp: string }> {
    // ...
    };
    }

Type Children Properly:

When you're specifying the type for children in your component props, use React.ReactNode. This type includes anything that can be rendered: numbers, strings, elements, or an array (or fragment) containing these types.

  • Code Example:

    type MyComponentProps = {
    children: React.ReactNode;
    };

    const MyComponent: React.FC<MyComponentProps> = ({ children }) => {
    // ...
    };

Type Form Elements Precisely:

In forms, make sure to use the right type for events. For example, use React.ChangeEvent<HTMLInputElement> for input changes. This ensures the event handler knows exactly what element it's dealing with.

  • Code Example:

    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    // Now you have all the properties of an input element in 'event.target'
    console.log(event.target.value);
    };

    // ...
    <input type="text" onChange={handleChange} />

Interface vs. Type Aliases:

Interfaces are better for public API's definition, like component props, because they can be extended or implemented. Type aliases are better when you need specific union or intersection types.

  • Code Example:

    interface MyComponentProps {
    // can be extended
    name: string;
    }

    interface MyStatefulComponentProps extends MyComponentProps {
    // extends the above interface
    age: number;
    }

Use enum for Component Variants:

Enums help manage component variants by providing a clear, limited set of values that a prop can have, making your components less error-prone and the code easier to understand.

  • Code Example:

    enum ButtonVariants {
    Primary = 'primary',
    Secondary = 'secondary',
    Danger = 'danger',
    }

    type ButtonProps = {
    variant: ButtonVariants;
    };

    const Button: React.FC<ButtonProps> = ({ variant }) => {
    // ...
    };

Strict Mode Compliance:

Turning on strict mode in TypeScript makes type checking more stringent. This helps catch mistakes before they become problems and encourages better coding habits.

  • Code Example:
    // tsconfig.json
    {
    "compilerOptions": {
    "strict": true
    // other options...
    }
    }

Type Third-Party Libraries Carefully:

When using libraries not written in TypeScript, look for their type definitions with @types/ prefix. If there are none, you may need to define the types yourself to use them safely in TypeScript.

  • Code Example:
    npm install @types/lodash --save-dev
    // This installs type definitions for 'lodash'

Keep Component Props Readonly:

Props should not be changed inside a component. Marking them as readonly ensures they're used correctly. This can be done with the Readonly type or by adding readonly before each property.

  • Code Example:

    type MyComponentProps = Readonly<{
    name: string;
    }>;

    // or

    interface MyComponentProps {
    readonly name: string;
    }