Github url

react-redux-typescript-guide

by piotrwitek

The complete guide to static typing in "React & Redux" apps using TypeScript

9.6K Stars 647 Forks Last release: Not found MIT License 311 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

React & Redux in TypeScript - Complete Guide _"This guide is a **living compendium** documenting the most important patterns and recipes on how to use **React** (and its Ecosystem) in a **functional style** using **TypeScript**. It will help you make your code **completely type-safe** while focusing on **inferring the types from implementation** so there is less noise coming from excessive type annotations and it's easier to write and maintain correct types in the long run."_ Join the community on Spectrum Join the chat at https://gitter.im/react-redux-typescript-guide/Lobby _Found it useful? Want more updates?_ **Show your support by giving a :star:**Buy Me a CoffeeBecome a Patron


**What's new?** :tada: _Now updated to support **TypeScript v3.7**_ :tada: :rocket: _Updated to [email protected] :rocket:


Goals

  • Complete type safety (with [ ```
  • -strict
    ](https://www.typescriptlang.org/docs/handbook/compiler-options.html) flag) without losing type information downstream through all the layers of our application (e.g. no type assertions or hacking with 
    any ``` type)
  • Make type annotations concise by eliminating redundancy in types using advanced TypeScript Language features like Type Inference and Control flow analysis
  • Reduce repetition and complexity of types with TypeScript focused complementary libraries

React, Redux, Typescript Ecosystem

  • typesafe-actions - Typesafe utilities for "action-creators" in Redux / Flux Architecture
  • utility-types - Collection of generic types for TypeScript, complementing built-in mapped types and aliases - think lodash for reusable types.
  • react-redux-typescript-scripts - dev-tools configuration files shared between projects based on this guide

Examples

Playground Project

Build Status

Check out our Playground Project located in the

/playground

folder. It contains all source files of the code examples found in the guide. They are all tested with the most recent version of TypeScript and 3rd party type-definitions (like

@types/react

or

@types/react-redux

) to ensure the examples are up-to-date and not broken with updated definitions (It's based on

create-react-app --typescript

).

Playground project was created so that you can simply clone the repository locally and immediately play around with all the component patterns found in the guide. It will help you to learn all the examples from this guide in a real project environment without the need to create complicated environment setup by yourself.

Contributing Guide

You can help make this project better by contributing. If you're planning to contribute please make sure to check our contributing guide: CONTRIBUTING.md

Funding

You can also help by funding issues. Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.

I highly recommend to add a bounty to the issue that you're waiting for to increase priority and attract contributors willing to work on it.

Let's fund issues in this repository


🌟 - New or updated section

Table of Contents


Installation

Type-Definitions for React & Redux

npm i -D @types/react @types/react-dom @types/react-redux

"react" -

@types/react

"react-dom" -

@types/react-dom

"redux" - (types included with npm package)*
"react-redux" -

@types/react-redux

*NB: Guide is based on types for Redux >= v4.x.x. To make it work with Redux v3.x.x please refer to this config)

⇧ back to top


React - Type-Definitions Cheatsheet

React.FC<props></props>

|

React.FunctionComponent<props></props>

Type representing a functional component

tsx const MyComponent: React.FC<props> = ...
</props>

React.Component<props state></props>

Type representing a class component

tsx class MyComponent extends React.Component<props state> { ...
</props>

React.ComponentType<props></props>

Type representing union of (React.FC | React.Component) - used in HOC

tsx
const withState = 

( WrappedComponent: React.ComponentType

, ) => { ...

React.ComponentProps<typeof xxx></typeof>

Gets Props type of a specified component XXX (WARNING: does not work with statically declared default props and generic props)

tsx type MyComponentProps = React.ComponentProps<typeof mycomponent>;
</typeof>

React.ReactElement

|

JSX.Element

Type representing a concept of React Element - representation of a native DOM component (e.g.

), or a user-defined composite component (e.g.

<mycomponent></mycomponent>

)

tsx const elementOnly: React.ReactElement = 

 || <mycomponent></mycomponent>;

React.ReactNode

Type representing any possible type of React node (basically ReactElement (including Fragments and Portals) + primitive JS types)

tsx const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || 

 || <mycomponent></mycomponent>; const Component = ({ children: React.ReactNode }) =\> ...

React.CSSProperties

Type representing style object in JSX - for css-in-js styles

tsx const styles: React.CSSProperties = { flexDirection: 'row', ... const element = 

### 

React.HTMLProps


Type representing Props of specified HTML Element - for extending HTML Elements ```tsx const Input: React.FC<props react.htmlprops>&gt; = props =&gt; { ... }</props>

<input about="{...}" accept="{...}" alt="{...}" ...>```

### 

React.ReactEventHandler


Type representing generic event handler - for declaring event handlers ```tsx const handleChange: React.ReactEventHandler<htmlinputelement> = (ev) =&gt; { ... } </htmlinputelement>

<input onchange="{handleChange}" ...>```

### 

React.XXXEvent


Type representing more specific event. Some common event examples:

ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent

. ```tsx const handleChange = (ev: React.MouseEvent<htmldivelement>) =&gt; { ... }

<div onmousemove="{handleChange}" ...></div>

In code above React.MouseEvent<htmldivelement> is type of mouse event, and this event happened on HTMLDivElement

⇧ back to top


React - Typing Patterns

Function Components - FC

- Counter Component

import * as React from 'react';

type Props = {
  label: string;
  count: number;
  onIncrement: () =&gt; void;
};

export const FCCounter: React.FC<props> = props =&gt; {
  const { label, count, onIncrement } = props;

  const handleIncrement = () =&gt; {
    onIncrement();
  };

  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onclick="{handleIncrement}">
        {`Increment`}
      </button>
    </div>

<p>);
};</p>
<pre class="">
[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#fccounter)

[⇧ back to top](#table-of-contents)

### - [Spreading attributes](https://facebook.github.io/react/docs/jsx-in-depth.html#spread-attributes) in Component

```tsx
import * as React from 'react';

type Props = {
  className?: string;
  style?: React.CSSProperties;
};

export const FCSpreadAttributes: React.FC<props> = props =&gt; {
  const { children, ...restProps } = props;

  return <div>{children}</div>;
};

</props></pre>
<p><a href="https://piotrwitek.github.io/react-redux-typescript-guide/#fcspreadattributes">⟩⟩⟩ demo</a></p>

<p><a href="https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/#table-of-contents">⇧ back to top</a></p>

<hr>

<h2>Class Components</h2>

<h3>- Class Counter Component</h3>
<pre class="tsx">import * as React from 'react';

type Props = {
  label: string;
};

type State = {
  count: number;
};

export class ClassCounter extends React.Component<props state> {
  readonly state: State = {
    count: 0,
  };

  handleIncrement = () =&gt; {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    const { handleIncrement } = this;
    const { label } = this.props;
    const { count } = this.state;

    return (
      <div>
        <span>
          {label}: {count}
        </span>
        <button type="button" onclick="{handleIncrement}">
          {`Increment`}
        </button>
      </div>
    );
  }
}

</props></pre>
<p><a href="https://piotrwitek.github.io/react-redux-typescript-guide/#classcounter">⟩⟩⟩ demo</a></p>

<p><a href="https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/#table-of-contents">⇧ back to top</a></p>

<h3>- Class Component with default props</h3>
<pre class="tsx">import * as React from 'react';

type Props = {
  label: string;
  initialCount: number;
};

type State = {
  count: number;
};

export class ClassCounterWithDefaultProps extends React.Component&lt;
  Props,
  State
&gt; {
  static defaultProps = {
    initialCount: 0,
  };

  readonly state: State = {
    count: this.props.initialCount,
  };

  handleIncrement = () =&gt; {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    const { handleIncrement } = this;
    const { label } = this.props;
    const { count } = this.state;

    return (
      <div>
        <span>
          {label}: {count}
        </span>
        <button type="button" onclick="{handleIncrement}">
          {`Increment`}
        </button>
      </div>
    );
  }
}

</pre>
<p><a href="https://piotrwitek.github.io/react-redux-typescript-guide/#classcounterwithdefaultprops">⟩⟩⟩ demo</a></p>

<p><a href="https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/#table-of-contents">⇧ back to top</a></p>

<hr>

<h2>Generic Components</h2>

<ul>
<li>easily create typed component variations and reuse common logic</li>
<li>common use case is a generic list components</li>
</ul>

<h3>- Generic List Component</h3>
<pre class="tsx">import * as React from 'react';

export interface GenericListProps<t> {
  items: T[];
  itemRenderer: (item: T) =&gt; JSX.Element;
}

export class GenericList<t> extends React.Component<genericlistprops>, {}&gt; {
  render() {
    const { items, itemRenderer } = this.props;

    return (
      <div>
        {items.map(itemRenderer)}
      </div>
    );
  }
}

</genericlistprops></t></t></pre>
<p><a href="https://piotrwitek.github.io/react-redux-typescript-guide/#genericlist">⟩⟩⟩ demo</a></p>

<p><a href="https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/#table-of-contents">⇧ back to top</a></p>

<hr>

<h2>Render Props</h2>

<blockquote>
<p>https://reactjs.org/docs/render-props.html</p>
</blockquote>

<h3>- Name Provider Component</h3>

<blockquote>
<p>simple component using children as a render prop</p>
</blockquote>
<pre class="tsx">import * as React from 'react';

interface NameProviderProps {
  children: (state: NameProviderState) =&gt; React.ReactNode;
}

interface NameProviderState {
  readonly name: string;
}

export class NameProvider extends React.Component<nameproviderprops nameproviderstate> {
  readonly state: NameProviderState = { name: 'Piotr' };

  render() {
    return this.props.children(this.state);
  }
}

</nameproviderprops></pre>
<p><a href="https://piotrwitek.github.io/react-redux-typescript-guide/#nameprovider">⟩⟩⟩ demo</a></p>

<p><a href="https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/#table-of-contents">⇧ back to top</a></p>

<h3>- Mouse Provider Component</h3>

<blockquote>
<p></p>
<pre class="">Mouse</pre> component found in <a href="https://reactjs.org/docs/render-props.html#use-render-props-for-cross-cutting-concerns">Render Props React Docs</a>
</blockquote>
<pre class="tsx">import * as React from 'react';

export interface MouseProviderProps {
  render: (state: MouseProviderState) =&gt; React.ReactNode;
}

interface MouseProviderState {
  readonly x: number;
  readonly y: number;
}

export class MouseProvider extends React.Component<mouseproviderprops mouseproviderstate> {
  readonly state: MouseProviderState = { x: 0, y: 0 };

  handleMouseMove = (event: React.MouseEvent<htmldivelement>) =&gt; {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };

  render() {
    return (
      <div style="{{" height: onmousemove="{this.handleMouseMove}">
        {/*
          Instead of providing a static representation of what <mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </mouse>
</div>
    );
  }
}

</htmldivelement></mouseproviderprops></pre>
<p><a href="https://piotrwitek.github.io/react-redux-typescript-guide/#mouseprovider">⟩⟩⟩ demo</a></p>

<p><a href="https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/#table-of-contents">⇧ back to top</a></p>

<hr>

<h2>Higher-Order Components</h2>

<blockquote>
<p>https://reactjs.org/docs/higher-order-components.html</p>
</blockquote>

<h3>- HOC wrapping a component</h3>

<p>Adds state to a stateless counter</p>
<pre class="tsx">import React from 'react';
import { Diff } from 'utility-types';

// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () =&gt; void;
}

export const withState = <baseprops extends injectedprops>(
  BaseComponent: React.ComponentType<baseprops>
) =&gt; {
  type HocProps = Diff<baseprops injectedprops> &amp; {
    // here you can extend hoc with new props
    initialCount?: number;
  };
  type HocState = {
    readonly count: number;
  };

  return class Hoc extends React.Component<hocprops hocstate> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withState(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;

    readonly state: HocState = {
      count: Number(this.props.initialCount) || 0,
    };

    handleIncrement = () =&gt; {
      this.setState({ count: this.state.count + 1 });
    };

    render() {
      const { ...restProps } = this.props;
      const { count } = this.state;

      return (
        <basecomponent count="{count}" injected onincrement="{this.handleIncrement}" as baseprops></basecomponent>
      );
    }
  };
};

</hocprops></baseprops></baseprops></baseprops></pre>
<details><summary><i>Click to expand</i></summary><p>

```tsx
import * as React from 'react';

import { withState } from '../hoc';
import { FCCounter } from '../components';

const FCCounterWithState = withState(FCCounter);

export default () =&gt; <fccounterwithstate label="{'FCCounterWithState'}"></fccounterwithstate>;

⇧ back to top

- HOC wrapping a component and injecting props

Adds error handling using componentDidCatch to any component

import React from 'react';

const MISSING_ERROR = 'Error was swallowed during propagation.';

export const withErrorBoundary = ( BaseComponent: React.ComponentType ) => { type HocProps = { // here you can extend hoc with new props }; type HocState = { readonly error: Error | null | undefined; };

return class Hoc extends React.Component { // Enhance component name for debugging and React-Dev-Tools static displayName = withErrorBoundary(${BaseComponent.name}); // reference to original wrapped component static readonly WrappedComponent = BaseComponent;

readonly state: HocState = {
  error: undefined,
};

componentDidCatch(error: Error | null, info: object) {
  this.setState({ error: error || new Error(MISSING_ERROR) });
  this.logErrorToCloud(error, info);
}

logErrorToCloud = (error: Error | null, info: object) =&gt; {
  // TODO: send error report to service provider
};

render() {
  const { children, ...restProps } = this.props;
  const { error } = this.state;

  if (error) {
    return <basecomponent as baseprops></basecomponent>;
  }

  return children;
}

}; };

Click to expand

import React, {useState} from 'react';

import { withErrorBoundary } from '../hoc';
import { ErrorMessage } from '../components';

const ErrorMessageWithErrorBoundary =
  withErrorBoundary(ErrorMessage);

const BrokenComponent = () =&gt; {
  throw new Error('I\'m broken! Don\'t render me.');
};

const BrokenButton = () =&gt; {
  const [shouldRenderBrokenComponent, setShouldRenderBrokenComponent] =
    useState(false);

  if (shouldRenderBrokenComponent) {
    return <brokencomponent></brokencomponent>;
  }

  return (
    <button type="button" onclick="{()"> {
        setShouldRenderBrokenComponent(true);
      }}
    &gt;
      {`Throw nasty error`}
    </button>
  );
};

export default () =&gt; (
  <errormessagewitherrorboundary>
    <brokenbutton></brokenbutton>
  </errormessagewitherrorboundary>
);

⇧ back to top

- Nested HOC - wrapping a component, injecting props and connecting to redux 🌟

Adds error handling using componentDidCatch to any component

import { RootState } from 'MyTypes';
import React from 'react';
import { connect } from 'react-redux';
import { Diff } from 'utility-types';
import { countersActions, countersSelectors } from '../features/counters';

// These props will be injected into the base component interface InjectedProps { count: number; onIncrement: () => void; }

export const withConnectedCount = ( BaseComponent: React.ComponentType ) => { const mapStateToProps = (state: RootState) => ({ count: countersSelectors.getReduxCounter(state.counters), });

const dispatchProps = { onIncrement: countersActions.increment, };

type HocProps = ReturnType & typeof dispatchProps & { // here you can extend ConnectedHoc with new props overrideCount?: number; };

class Hoc extends React.Component { // Enhance component name for debugging and React-Dev-Tools static displayName = withConnectedCount(${BaseComponent.name}); // reference to original wrapped component static readonly WrappedComponent = BaseComponent;

render() {
  const { count, onIncrement, overrideCount, ...restProps } = this.props;

  return (
    <basecomponent count="{overrideCount" injected onincrement="{onIncrement}" as baseprops></basecomponent>
  );
}

}

const ConnectedHoc = connect< ReturnType, typeof dispatchProps, // use "undefined" if NOT using dispatchProps Diff, RootState >( mapStateToProps, dispatchProps )(Hoc);

return ConnectedHoc; };

Click to expand

import * as React from 'react';

import { withConnectedCount } from '../hoc';
import { FCCounter } from '../components';

const FCCounterWithConnectedCount = withConnectedCount(FCCounter);

export default () =&gt; (
  <fccounterwithconnectedcount overridecount="{5}" label="{'FCCounterWithState'}"></fccounterwithconnectedcount>
);

⇧ back to top


Redux Connected Components

- Redux connected counter

import Types from 'MyTypes';
import { connect } from 'react-redux';

import { countersActions, countersSelectors } from '../features/counters'; import { FCCounter } from '../components';

const mapStateToProps = (state: Types.RootState) => ({ count: countersSelectors.getReduxCounter(state.counters), });

const dispatchProps = { onIncrement: countersActions.increment, };

export const FCCounterConnected = connect( mapStateToProps, dispatchProps )(FCCounter);

Click to expand

import * as React from 'react';

import { FCCounterConnected } from '.';

export default () =&gt; <fccounterconnected label="{'FCCounterConnected'}"></fccounterconnected>;

⇧ back to top

- Redux connected counter with own props

import Types from 'MyTypes';
import { connect } from 'react-redux';

import { countersActions, countersSelectors } from '../features/counters'; import { FCCounter } from '../components';

type OwnProps = { initialCount?: number; };

const mapStateToProps = (state: Types.RootState, ownProps: OwnProps) => ({ count: countersSelectors.getReduxCounter(state.counters) + (ownProps.initialCount || 0), });

const dispatchProps = { onIncrement: countersActions.increment, };

export const FCCounterConnectedOwnProps = connect( mapStateToProps, dispatchProps )(FCCounter);

Click to expand

import * as React from 'react';

import { FCCounterConnectedOwnProps } from '.';

export default () =&gt; (
  <fccounterconnectedownprops label="{'FCCounterConnectedOwnProps'}" initialcount="{10}"></fccounterconnectedownprops>
);

⇧ back to top

- Redux connected counter with
redux-thunk
integration

import Types from 'MyTypes';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import * as React from 'react';

import { countersActions } from '../features/counters';

// Thunk Action const incrementWithDelay = () => async (dispatch: Dispatch): Promise => { setTimeout(() => dispatch(countersActions.increment()), 1000); };

const mapStateToProps = (state: Types.RootState) => ({ count: state.counters.reduxCounter, });

const mapDispatchToProps = (dispatch: Dispatch<types.rootaction>) => bindActionCreators( { onIncrement: incrementWithDelay, }, dispatch );

type Props = ReturnType & ReturnType & { label: string; };

export const FCCounter: React.FC = props => { const { label, count, onIncrement } = props;

const handleIncrement = () => { // Thunk action is correctly typed as promise onIncrement().then(() => { // ... }); };

return (

{label}: {count} {Increment}
); };

export const FCCounterConnectedBindActionCreators = connect( mapStateToProps, mapDispatchToProps )(FCCounter);

</types.rootaction>

Click to expand

import * as React from 'react';

import { FCCounterConnectedBindActionCreators } from '.';

export default () =&gt; (
  <fccounterconnectedbindactioncreators label="{'FCCounterConnectedBindActionCreators'}"></fccounterconnectedbindactioncreators>
);

⇧ back to top

Context

https://reactjs.org/docs/context.html

ThemeContext

import * as React from 'react';

export type Theme = React.CSSProperties;

type Themes = { dark: Theme; light: Theme; };

export const themes: Themes = { dark: { color: 'black', backgroundColor: 'white', }, light: { color: 'white', backgroundColor: 'black', }, };

export type ThemeContextProps = { theme: Theme; toggleTheme?: () => void }; const ThemeContext = React.createContext({ theme: themes.light });

export default ThemeContext;

⇧ back to top

ThemeProvider

import React from 'react';
import ThemeContext, { themes, Theme } from './theme-context';
import ToggleThemeButton from './theme-consumer';

interface State { theme: Theme; } export class ThemeProvider extends React.Component { readonly state: State = { theme: themes.light };

toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.light ? themes.dark : themes.light, })); }

render() { const { theme } = this.state; const { toggleTheme } = this; return ( <themecontext.provider value="{{" theme toggletheme> </themecontext.provider> ); } }

⇧ back to top

ThemeConsumer

import * as React from 'react';
import ThemeContext from './theme-context';

type Props = {};

export default function ToggleThemeButton(props: Props) { return ( <themecontext.consumer> {({ theme, toggleTheme }) => } </themecontext.consumer> ); }

ThemeConsumer in class component

import * as React from 'react';
import ThemeContext from './theme-context';

type Props = {};

export class ToggleThemeButtonClass extends React.Component { static contextType = ThemeContext; context!: React.ContextType;

render() { const { theme, toggleTheme } = this.context; return ( Toggle Theme ); } }

Implementation with Hooks

⇧ back to top

Hooks

https://reactjs.org/docs/hooks-intro.html

- useState

https://reactjs.org/docs/hooks-reference.html#usestate

import * as React from 'react';

type Props = { initialCount: number };

export default function Counter({initialCount}: Props) { const [count, setCount] = React.useState(initialCount); return ( <> Count: {count} setCount(initialCount)}>Reset setCount(prevCount => prevCount + 1)}>+ setCount(prevCount => prevCount - 1)}>- > ); }

⇧ back to top

- useReducer

Hook for state management like Redux in a function component.

import * as React from 'react';

interface State { count: number; }

type Action = { type: 'reset' } | { type: 'increment' } | { type: 'decrement' };

function reducer(state: State, action: Action): State { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: throw new Error(); } }

interface CounterProps { initialCount: number; }

function Counter({ initialCount }: CounterProps) { const [state, dispatch] = React.useReducer(reducer, { count: initialCount, });

return ( <> Count: {state.count} dispatch({ type: 'reset' })}>Reset dispatch({ type: 'increment' })}>+ dispatch({ type: 'decrement' })}>- > ); }

export default Counter;

⇧ back to top

- useContext

https://reactjs.org/docs/hooks-reference.html#usecontext

import * as React from 'react';
import ThemeContext from '../context/theme-context';

type Props = {};

export default function ThemeToggleButton(props: Props) { const { theme, toggleTheme } = React.useContext(ThemeContext); return ( Toggle Theme ); }

⇧ back to top


Redux - Typing Patterns

Store Configuration

Create Global Store Types

RootState
- type representing root state-tree

Can be imported in connected components to provide type-safety to Redux

connect
function

RootAction
- type representing union type of all action objects

Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics

import { StateType, ActionType } from 'typesafe-actions';

declare module 'MyTypes' { export type Store = StateType; export type RootAction = ActionType; export type RootState = StateType>; }

declare module 'typesafe-actions' { interface Types { RootAction: ActionType; } }

⇧ back to top

Create Store

When creating a store instance we don't need to provide any additional types. It will set-up a type-safe Store instance using type inference.

The resulting store instance methods like

getState
or
dispatch
will be type checked and will expose all type errors
import { RootAction, RootState, Services } from 'MyTypes';
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { createBrowserHistory } from 'history';
import { routerMiddleware as createRouterMiddleware } from 'connected-react-router';

import { composeEnhancers } from './utils'; import rootReducer from './root-reducer'; import rootEpic from './root-epic'; import services from '../services';

// browser history export const history = createBrowserHistory();

export const epicMiddleware = createEpicMiddleware< RootAction, RootAction, RootState, Services >({ dependencies: services, });

const routerMiddleware = createRouterMiddleware(history);

// configure middlewares const middlewares = [epicMiddleware, routerMiddleware]; // compose enhancers const enhancer = composeEnhancers(applyMiddleware(...middlewares));

// rehydrate state on app start const initialState = {};

// create store const store = createStore(rootReducer(history), initialState, enhancer);

epicMiddleware.run(rootEpic);

// export store singleton instance export default store;


Action Creators 🌟

We'll be using a battle-tested helper library

typesafe-actions
Latest Stable Version NPM Downloads that's designed to make it easy and fun working with Redux in TypeScript.

To learn more please check this in-depth tutorial: Typesafe-Actions - Tutorial!

A solution below is using a simple factory function to automate the creation of type-safe action creators. The goal is to decrease maintenance effort and reduce code repetition of type annotations for actions and creators. The result is completely typesafe action-creators and their actions.

/* eslint-disable */
import { action } from 'typesafe-actions';

import { ADD, INCREMENT } from './constants';

/* SIMPLE API */

export const increment = () => action(INCREMENT); export const add = (amount: number) => action(ADD, amount);

/* ADVANCED API */

// More flexible allowing to create complex actions more easily // use can use "action-creator" instance in place of "type constant" // e.g. case getType(increment): return action.payload; // This will allow to completely eliminate need for "constants" in your application, more info here: // https://github.com/piotrwitek/typesafe-actions#constants

import { createAction } from 'typesafe-actions'; import { Todo } from '../todos/models';

export const emptyAction = createAction(INCREMENT)(); export const payloadAction = createAction(ADD)(); export const payloadMetaAction = createAction(ADD)();

export const payloadCreatorAction = createAction( 'TOGGLE_TODO', (todo: Todo) => todo.id )();

Click to expand

import store from '../../store';
import { countersActions as counter } from '../counters';

// store.dispatch(counter.increment(1)); // Error: Expected 0 arguments, but got 1.
store.dispatch(counter.increment()); // OK

// store.dispatch(counter.add()); // Error: Expected 1 arguments, but got 0.
store.dispatch(counter.add(1)); // OK

⇧ back to top


Reducers

State with Type-level Immutability

Declare reducer

State
type with
readonly
modifier to get compile time immutability
ts
export type State = {
  readonly counter: number;
  readonly todos: ReadonlyArray;
};

Readonly modifier allow initialization, but will not allow reassignment by highlighting compiler errors ```ts export const initialState: State = { counter: 0, }; // OK

initialState.counter = 3; // TS Error: cannot be mutated ```

It's great for Arrays in JS because it will error when using mutator methods like (

push
,
pop
,
splice
, ...), but it'll still allow immutable methods like (
concat
,
map
,
slice
,...).
ts
state.todos.push('Learn about tagged union types') // TS Error: Property 'push' does not exist on type 'ReadonlyArray'
const newTodos = state.todos.concat('Learn about tagged union types') // OK

Caveat -
Readonly
is not recursive

This means that the

readonly
modifier doesn't propagate immutability down the nested structure of objects. You'll need to mark each property on each level explicitly.

TIP: use

Readonly
or
ReadonlyArray
Mapped types
export type State = Readonly>,
}>;

state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated

Solution - recursive
Readonly
is called
DeepReadonly

To fix this we can use

DeepReadonly
type (available from

utility-types
).
import { DeepReadonly } from 'utility-types';

export type State = DeepReadonly;

state.containerObject = { innerValue: 1 }; // TS Error: cannot be mutated state.containerObject.innerValue = 1; // TS Error: cannot be mutated state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods

⇧ back to top

Typing reducer

to understand following section make sure to learn about Type Inference, Control flow analysis and Tagged union types

import { combineReducers } from 'redux';
import { ActionType } from 'typesafe-actions';

import { Todo, TodosFilter } from './models'; import * as actions from './actions'; import { ADD, CHANGE_FILTER, TOGGLE } from './constants';

export type TodosAction = ActionType;

export type TodosState = Readonly; const initialState: TodosState = { todos: [], todosFilter: TodosFilter.All, };

export default combineReducers({ todos: (state = initialState.todos, action) => { switch (action.type) { case ADD: return [...state, action.payload];

  case TOGGLE:
    return state.map(item =&gt;
      item.id === action.payload
        ? { ...item, completed: !item.completed }
        : item
    );

  default:
    return state;
}

}, todosFilter: (state = initialState.todosFilter, action) => { switch (action.type) { case CHANGE_FILTER: return action.payload;

  default:
    return state;
}

}, });

⇧ back to top

Typing reducer with
typesafe-actions

Notice we are not required to use any generic type parameter in the API. Try to compare it with regular reducer as they are equivalent.

import { combineReducers } from 'redux';
import { createReducer } from 'typesafe-actions';

import { Todo, TodosFilter } from './models'; import { ADD, CHANGE_FILTER, TOGGLE } from './constants';

export type TodosState = Readonly; const initialState: TodosState = { todos: [], todosFilter: TodosFilter.All, };

const todos = createReducer(initialState.todos) .handleType(ADD, (state, action) => [...state, action.payload]) .handleType(TOGGLE, (state, action) => state.map(item => item.id === action.payload ? { ...item, completed: !item.completed } : item ) );

const todosFilter = createReducer(initialState.todosFilter).handleType( CHANGE_FILTER, (state, action) => action.payload );

export default combineReducers({ todos, todosFilter, });

⇧ back to top

Testing reducer

import {
  todosReducer as reducer,
  todosActions as actions,
  TodosState,
} from './';

/**

  • FIXTURES
  • / const getInitialState = (initial?: Partial) => reducer(initial as TodosState, {} as any);

/**

  • STORIES

  • / describe('Todos Stories', () => { describe('initial state', () => { it('should match a snapshot', () => {

    const initialState = getInitialState();
    expect(initialState).toMatchSnapshot();

    }); });

    describe('adding todos', () => { it('should add a new todo as the first element', () => {

    const initialState = getInitialState();
    expect(initialState.todos).toHaveLength(0);
    const state = reducer(initialState, actions.add('new todo'));
    expect(state.todos).toHaveLength(1);
    expect(state.todos[0].title).toEqual('new todo');

    }); });

    describe('toggling completion state', () => { it('should mark active todo as complete', () => {

    const activeTodo = { id: '1', completed: false, title: 'active todo' };
    const initialState = getInitialState({ todos: [activeTodo] });
    expect(initialState.todos[0].completed).toBeFalsy();
    const state1 = reducer(initialState, actions.toggle(activeTodo.id));
    expect(state1.todos[0].completed).toBeTruthy();

    }); }); });

⇧ back to top


Async Flow with
redux-observable

Typing epics

import { RootAction, RootState, Services } from 'MyTypes';
import { Epic } from 'redux-observable';
import { tap, ignoreElements, filter } from 'rxjs/operators';
import { isOfType } from 'typesafe-actions';

import { todosConstants } from '../todos';

// contrived example!!! export const logAddAction: Epic = ( action$, state$, { logger } ) => action$.pipe( filter(isOfType(todosConstants.ADD)), // action is narrowed to: { type: "ADD_TODO"; payload: string; } tap(action => { logger.log( action type must be equal: ${todosConstants.ADD} === ${action.type} ); }), ignoreElements() );

⇧ back to top

Testing epics

import { StateObservable, ActionsObservable } from 'redux-observable';
import { RootState, Services, RootAction } from 'MyTypes';
import { Subject } from 'rxjs';

import { add } from './actions'; import { logAddAction } from './epics';

// Simple typesafe mock of all the services, you dont't need to mock anything else // It is decoupled and reusable for all your tests, just put it in a separate file const services = { logger: { log: jest.fn(), }, localStorage: { loadState: jest.fn(), saveState: jest.fn(), }, };

describe('Todos Epics', () => { let state$: StateObservable;

beforeEach(() => { state$ = new StateObservable( new Subject(), undefined as any ); });

describe('logging todos actions', () => { beforeEach(() => { services.logger.log.mockClear(); });

it('should call the logger service when adding a new todo', done =&gt; {
  const addTodoAction = add('new todo');
  const action$ = ActionsObservable.of(addTodoAction);

  logAddAction(action$, state$, services)
    .toPromise()
    .then((outputAction: RootAction) =&gt; {
      expect(services.logger.log).toHaveBeenCalledTimes(1);
      expect(services.logger.log).toHaveBeenCalledWith(
        'action type must be equal: todos/ADD === todos/ADD'
      );
      // expect output undefined because we're using "ignoreElements" in epic
      expect(outputAction).toEqual(undefined);
      done();
    });
});

}); });

⇧ back to top


Selectors with
reselect

import { createSelector } from 'reselect';

import { TodosState } from './reducer';

export const getTodos = (state: TodosState) => state.todos;

export const getTodosFilter = (state: TodosState) => state.todosFilter;

export const getFilteredTodos = createSelector(getTodos, getTodosFilter, (todos, todosFilter) => { switch (todosFilter) { case 'completed': return todos.filter(t => t.completed); case 'active': return todos.filter(t => !t.completed);

default:
  return todos;

} });

⇧ back to top


Connect with
react-redux

Typing connected component

NOTE: Below you'll find only a short explanation of concepts behind typing

connect
. For more real-world examples please check Redux Connected Components section.

import MyTypes from 'MyTypes';

import { bindActionCreators, Dispatch, ActionCreatorsMapObject } from 'redux'; import { connect } from 'react-redux';

import { countersActions } from '../features/counters'; import { FCCounter } from '../components';

// Type annotation for "state" argument is mandatory to check // the correct shape of state object and injected props you can also // extend connected component Props interface by annotating ownProps argument const mapStateToProps = (state: MyTypes.RootState, ownProps: FCCounterProps) => ({ count: state.counters.reduxCounter, });

// "dispatch" argument needs an annotation to check the correct shape // of an action object when using dispatch function const mapDispatchToProps = (dispatch: Dispatch<mytypes.rootaction>) => bindActionCreators({ onIncrement: countersActions.increment, }, dispatch);

// shorter alternative is to use an object instead of mapDispatchToProps function const dispatchToProps = { onIncrement: countersActions.increment, };

// Notice we don't need to pass any generic type parameters to neither // the connect function below nor map functions declared above // because type inference will infer types from arguments annotations automatically // This is much cleaner and idiomatic approach export const FCCounterConnected = connect(mapStateToProps, mapDispatchToProps)(FCCounter);

// You can add extra layer of validation of your action creators // by using bindActionCreators generic type parameter and RootAction type const mapDispatchToProps = (dispatch: Dispatch<mytypes.rootaction>) => bindActionCreators>({ invalidActionCreator: () => 1, // Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... } }, dispatch);

</mytypes.rootaction></mytypes.rootaction>

Typing connected component with
redux-thunk
integration

NOTE: When using thunk action creators you need to use

bindActionCreators
. Only this way you can get corrected dispatch props type signature like below.

WARNING: As of now (Apr 2019)

bindActionCreators
signature of the latest
redux-thunk
release will not work as below, you need to use updated type definitions that you can find here
/playground/typings/redux-thunk/index.d.ts
and then add
paths
overload in your tsconfig like this:
"paths":{"redux-thunk":["typings/redux-thunk"]}
.

const thunkAsyncAction = () => async (dispatch: Dispatch): Promise => {
  // dispatch actions, return Promise, etc.
}

const mapDispatchToProps = (dispatch: Dispatch<types.rootaction>) => bindActionCreators( { thunkAsyncAction, }, dispatch );

type DispatchProps = ReturnType; // { thunkAsyncAction: () => Promise; }

/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */ // { thunkAsyncAction: () => (dispatch: Dispatch) => Promise; } </types.rootaction>

⇧ back to top


Configuration & Dev Tools

Common Npm Scripts

Common TS-related npm scripts shared across projects

"prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease fix code formatting by running:\nnpm run prettier:fix\n'; exit 1)",
"prettier:fix": "prettier --write 'src/**/*.ts'",
"lint": "tslint -p ./",
"tsc": "tsc -p ./ --noEmit",
"tsc:watch": "tsc -p ./ --noEmit -w",
"test": "jest --config jest.config.json",
"test:watch": "jest --config jest.config.json --watch",
"test:update": "jest --config jest.config.json -u"
"ci-check": "npm run prettier && npm run lint && npm run tsc && npm run test",

⇧ back to top

tsconfig.json

We have our own recommended

tsconfig.json
that you can easily add to your project thanks to
react-redux-typescript-scripts
package.
Click to expand

{
  "include": [
    "src",
    "typings"
  ],
  "exclude": [
    "src/**/*.spec.*"
  ],
  "extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json",
  "compilerOptions": {}
}

⇧ back to top

TSLib

https://www.npmjs.com/package/tslib

This library will cut down on your bundle size, thanks to using external runtime helpers instead of adding them per each file.

Installation

npm i tslib

Then add this to your

tsconfig.json
:
ts
"compilerOptions": {
  "importHelpers": true
}

⇧ back to top

TSLint

https://palantir.github.io/tslint/

Installation

npm i -D tslint

For React project you should add additional

react
specific rules:
npm i -D tslint-react
https://github.com/palantir/tslint-react

We have our own recommended config that you can easily add to your project thanks to

react-redux-typescript-scripts
package.

tslint.json

Click to expand

{
  "extends": [
    "./node_modules/react-redux-typescript-scripts/tslint.json",
    "./node_modules/react-redux-typescript-scripts/tslint-react.json"
  ],
  "rules": {
    // you can further customize options here
  }
}

⇧ back to top

ESLint

https://eslint.org/
https://typescript-eslint.io

Installation

npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

We have our own recommended config that will automatically add a parser & plugin for TypeScript thanks to

react-redux-typescript-scripts
package.

.eslintrc

Click to expand

{
  "extends": [
    "react-app",
    "./node_modules/react-redux-typescript-scripts/eslint.js"
  ],
  "rules": {
    // you can further customize options here
  }
}

⇧ back to top

Jest

https://jestjs.io/

Installation

npm i -D jest ts-jest @types/jest

jest.config.json

Click to expand

{
  "verbose": true,
  "transform": {
    ".(ts|tsx)": "ts-jest"
  },
  "testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$",
  "moduleFileExtensions": ["ts", "tsx", "js"],
  "moduleNameMapper": {
    "^Components/(.*)": "./src/components/$1"
  },
  "globals": {
    "window": {},
    "ts-jest": {
      "tsConfig": "./tsconfig.json"
    }
  },
  "setupFiles": ["./jest.stubs.js"],
  "testURL": "http://localhost/"
}

jest.stubs.js

Click to expand

// Global/Window object Stubs for Jest
window.matchMedia = window.matchMedia || function () {
  return {
    matches: false,
    addListener: function () { },
    removeListener: function () { },
  };
};

window.requestAnimationFrame = function (callback) {
  setTimeout(callback);
};

window.localStorage = {
  getItem: function () { },
  setItem: function () { },
};

Object.values = () =&gt; [];

⇧ back to top

Style Guides

"react-styleguidist"

⟩⟩⟩ styleguide.config.js

⟩⟩⟩ demo

⇧ back to top


Recipes

General Tips

- should I still use React.PropTypes in TS?

No. With TypeScript, using PropTypes is an unnecessary overhead. When declaring Props and State interfaces, you will get complete intellisense and design-time safety with static type checking. This way you'll be safe from runtime errors and you will save a lot of time on debugging. Additional benefit is an elegant and standardized method of documenting your component public API in the source code.

⇧ back to top

- when to use
interface
declarations and when
type
aliases?

From practical side, using

interface
declaration will create an identity (interface name) in compiler errors, on the contrary
type
aliases doesn't create an identity and will be unwinded to show all the properties and nested types it consists of.
Although I prefer to use
type
most of the time there are some places this can become too noisy when reading compiler errors and that's why I like to leverage this distinction to hide some of not so important type details in errors using interfaces identity. Related
ts-lint
rule: https://palantir.github.io/tslint/rules/interface-over-type-literal/

⇧ back to top

- what's better default or named exports?

A common flexible solution is to use module folder pattern, because you can leverage both named and default import when you see fit.
With this solution you'll achieve better encapsulation and be able to safely refactor internal naming and folders structure without breaking your consumer code:

// 1. create your component files (`select.tsx`) using default export in some folder:

// components/select.tsx const Select: React.FC = (props) => { ... export default Select;

// 2. in this folder create an index.ts file that will re-export components with named exports:

// components/index.ts export { default as Select } from './select'; ...

// 3. now you can import your components in both ways, with named export (better encapsulation) or using default export (internal access):

// containers/container.tsx import { Select } from '@src/components'; or import Select from '@src/components/select'; ...

⇧ back to top

- how to best initialize class instance or static properties?

Prefered modern syntax is to use class Property Initializers
```tsx class ClassCounterWithInitialCount extends React.Component { // default props using Property Initializers static defaultProps: DefaultProps = { className: 'default-class', initialCount: 0, };

// initial state using Property Initializers state: State = { count: this.props.initialCount, }; ... } ```

⇧ back to top

- how to best declare component handler functions?

Prefered modern syntax is to use Class Fields with arrow functions

tsx
class ClassCounter extends React.Component {
// handlers using Class Fields with arrow functions
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
  ...
}

⇧ back to top

Ambient Modules Tips

Imports in ambient modules

For type augmentation imports should stay outside of module declaration. ```ts import { Operator } from 'rxjs/Operator'; import { Observable } from 'rxjs/Observable';

declare module 'rxjs/Subject' { interface Subject { lift(operator: Operator): Observable; } } ```

When creating 3rd party type-definitions all the imports should be kept inside the module declaration, otherwise it will be treated as augmentation and show error

declare module "react-custom-scrollbars" {
    import * as React from "react";
    export interface positionValues {
    ...

⇧ back to top

Type-Definitions Tips

Missing type-definitions error

if you cannot find types for a third-party module you can provide your own types or disable type-checking for this module using Shorthand Ambient Modules

// typings/modules.d.ts
declare module 'MyTypes';
declare module 'react-test-renderer';

Using custom
d.ts
files for npm modules

If you want to use an alternative (customized) type-definitions for some npm module (that usually comes with it's own type-definitions), you can do it by adding an override in

paths
compiler option.
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "redux": ["typings/redux"], // use an alternative type-definitions instead of the included one
      ...
    },
    ...,
  }
}

⇧ back to top

Type Augmentation Tips

Strategies to fix issues coming from external type-definitions files (*.d.ts)

Augmenting library internal declarations - using relative import

// added missing autoFocus Prop on Input component in "[email protected]" npm package
declare module '../node_modules/antd/lib/input/Input' {
  export interface InputProps {
    autoFocus?: boolean;
  }
}

Augmenting library public declarations - using node_modules import

// fixed broken public type-definitions in "[email protected]" npm package
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';

declare module 'rxjs/Subject' { interface Subject { lift(operator: Operator): Observable; } }

More advanced scenarios for working with vendor type-definitions can be found here Official TypeScript Docs

⇧ back to top


Tutorials & Articles

Curated list of relevant in-depth tutorials

Higher-Order Components: - https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb

⇧ back to top


Contributors

Thanks goes to these wonderful people (emoji key):

|
Piotrek Witek

πŸ’» πŸ“– πŸ€” πŸ‘€ πŸ’¬ |
Kazz Yokomizo

πŸ’΅ πŸ” |
Jake Boone

πŸ“– |
Amit Dahan

πŸ“– |
gulderov

πŸ“– |
Erik Pearson

πŸ“– |
Bryan Mason

πŸ“– | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | |
Jakub Chodorowicz

πŸ’» |
Oleg Maslov

πŸ› |
Aaron Westbrook

πŸ› |
Peter Blazejewicz

πŸ“– |
Solomon White

πŸ“– |
Levi Rocha

πŸ“– |
Sudachi-kun

πŸ’΅ | |
Sosuke Suzuki

πŸ’» |
Tom Rathbone

πŸ“– |
Arshad Kazmi

πŸ“– |
JeongUkJae

πŸ“– | <!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the all-contributors specification. Contributions of any kind welcome!


MIT License

Copyright (c) 2017 Piotr Witek [email protected] (http://piotrwitek.github.io)

```

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.