Skip to main content

typescript-with-redux-mobx

TypeScript with Redux:

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;
    }

Connect with Typed Props:

When you use the connect function from react-redux, you can specify the types of the state props and dispatch props that your component will receive. This ensures that your component is expecting the right types from the Redux store.

  • Code Example:

    interface StateProps {
    todos: Todo[];
    }

    interface DispatchProps {
    toggleTodo: (id: number) => void;
    }

    const mapStateToProps = (state: AppState): StateProps => ({
    todos: state.todos,
    });

    const mapDispatchToProps = (dispatch: Dispatch<Action>): DispatchProps => ({
    toggleTodo: (id) => dispatch(toggleTodoAction(id)),
    });

    // Now connect is fully typed with StateProps and DispatchProps
    connect(mapStateToProps, mapDispatchToProps)(MyComponent);

Use of TypeScript Enums:

Enums provide a way to handle sets of constants, like action types, and ensure that they are managed in a type-safe manner. This can prevent issues like typos in action types.

  • Code Example:

    enum TodoActionTypes {
    ADD_TODO = 'ADD_TODO',
    REMOVE_TODO = 'REMOVE_TODO',
    TOGGLE_TODO = 'TOGGLE_TODO',
    }

    // Use enums to dispatch actions
    dispatch({ type: TodoActionTypes.ADD_TODO, payload: { text: 'New Task' } });

Handling Async Actions:

When dealing with asynchronous actions, such as those that interact with an API, it's common to have action types for the request, success, and failure phases. Each action type should be typed to match the expected structure.

  • Code Example:

    enum TodoAsyncActionTypes {
    FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST',
    FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS',
    FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE',
    }

    // Action types are clearly defined for each phase of the async action

Type Guards in Reducers:

Type guards are a TypeScript feature that narrows down the type of an object within a conditional block. Use these in your reducers to ensure that each action is handled correctly according to its type.

  • Code Example:
    function todoReducer(state: TodoState, action: TodoActions): TodoState {
    if (action.type === TodoActionTypes.ADD_TODO) {
    // TypeScript knows the structure of action.payload here
    }
    // ...
    }

Typed Hooks:

Using the typed versions of useSelector and useDispatch hooks helps enforce type safety in your components. This means that your component will know the types of data it reads from the Redux store and the actions it can dispatch.

  • Code Example:
    const todos = useSelector((state: AppState) => state.todos);
    const dispatch = useDispatch<Dispatch<TodoActions>>();

Typed Middleware:

Custom middleware in Redux can be typed to make sure the actions and state passed through them are expected. This helps maintain the integrity of your middleware logic.

  • Code Example:
    const myMiddleware: Middleware<{}, AppState> = store => next => action => {
    // Your middleware logic with type safety
    return next(action);
    };

Modularizing Store Types:

Keep your types organized by defining them close to where they are used, such as with their respective reducers and selectors. Then combine them using TypeScript's utility types when constructing your store.

  • Code Example:

    // In todosReducer.ts
    export interface TodosState {
    todos: Todo[];
    }

    // In another file where you combine reducers
    import { TodosState } from './todosReducer';

    export interface AppState {
    todos: TodosState;
    // ... other state slices
    }

Using Redux Toolkit with TypeScript:

Redux Toolkit is designed to work well with TypeScript and simplifies the configuration of your store, writing reducers, and creating actions.

  • Code Example:

    import { configureStore } from '@reduxjs/toolkit';

    const store = configureStore({
    reducer: {
    // Your reducers go here
    },
    });

    // The store is now typed and ready to use with TypeScript